✨ (bootsier): Añade componentes esenciales

- Añade Dialog como modal de Bootstrap, con el JS de soporte para
  `htmx:confirm` y el saneado de su posición fija.
- Adapta Nav, Navbar y Dropdown al reuso de los tipos movidos al core, y
  traduce BootsierColors/Intent en Button y Badge, con soporte para
  Light/Dark vía with_color().
- Reexporta en el tema los componentes de PageTop que faltaban: Block,
  Breadcrumb, Messages, Pager, Table y form::Number.
This commit is contained in:
Manuel Cillero 2026-08-28 19:46:56 +02:00
parent eb63a7ef37
commit c0a5a8c3ab
47 changed files with 2135 additions and 1818 deletions

View file

@ -6,14 +6,18 @@ use crate::theme::*;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// Componente para crear una **barra de navegación**.
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).
///
/// 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).
///
/// Si no contiene elementos, el componente **no se renderiza**.
/// 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).
///
/// # Ejemplos
///
@ -52,9 +56,9 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = bs::navbar::Brand::new()
/// let brand = Brand::new()
/// .with_title(Lc::n("PageTop"))
/// .with_route(Some("/".into()));
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_item(bs::navbar::Item::nav(
@ -79,14 +83,15 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = bs::navbar::Brand::new()
/// let brand = Brand::new()
/// .with_title(Lc::n("Intranet"))
/// .with_route(Some("/".into()));
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_right(brand)
/// .with_expand(BreakPoint::LG)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::pills()
/// bs::Nav::new()
/// .with_kind(bs::nav::Kind::Pills)
/// .with_item(bs::nav::Item::link(Lc::n("Dashboard"), "/dashboard"))
/// .with_item(bs::nav::Item::link(Lc::n("Users"), "/users"))
/// ));
@ -122,9 +127,9 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = bs::navbar::Brand::new()
/// let brand = Brand::new()
/// .with_title(Lc::n("Main App"))
/// .with_route(Some("/".into()));
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_position(bs::navbar::Position::FixedTop)
@ -135,265 +140,224 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// .with_item(bs::nav::Item::link(Lc::n("Stock"), "/stock"))
/// ));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Navbar {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve el punto de ruptura configurado.
expand: BreakPoint,
/// Devuelve la disposición configurada para la barra de navegación.
layout: bs::navbar::Layout,
/// Devuelve la posición configurada para la barra de navegación.
position: bs::navbar::Position,
/// 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.
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
// Clases CSS por defecto para la barra de navegación.
self.alter_prop(PropsOp::prepend_classes({
let mut classes = "navbar".to_string();
self.expand().push_to(&mut classes, "navbar-expand", "");
self.position().push_to(&mut classes);
classes
}));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
// Botón de despliegue (colapso u offcanvas) para la barra.
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
let id_content_target = util::join!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false")
} else {
None
};
html! {
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[Lc::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{
span class="navbar-toggler-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();
Ok(html! {
nav (self.props()) {
div class="container-fluid" {
@match self.layout() {
// Barra más sencilla: sólo contenido.
bs::navbar::Layout::Simple => {
(items)
},
// Barra sencilla que se puede contraer/expandir.
bs::navbar::Layout::SimpleToggle => {
@let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con marca a la izquierda, siempre visible.
bs::navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
(items)
},
// Barra con marca a la izquierda y botón a la derecha.
bs::navbar::Layout::BrandLeft(brand) => {
@let id_content = util::join!(id, "-content");
(brand.render(cx).await)
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con botón a la izquierda y marca a la derecha.
bs::navbar::Layout::BrandRight(brand) => {
@let id_content = util::join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx).await)
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
bs::navbar::Layout::Offcanvas(offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items())).await)
}
},
// Barra con marca a la izquierda y contenido en offcanvas.
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx).await)
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items())).await)
}
},
// Barra con contenido en offcanvas y marca a la derecha.
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
(brand.render(cx).await)
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(self.items())).await)
}
},
}
}
}
})
}
}
impl Navbar {
/// Crea una barra de navegación **simple**, sin marca y sin botón.
pub fn simple() -> Self {
Self::default().with_layout(bs::navbar::Layout::Simple)
}
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
pub fn simple_toggle() -> Self {
Self::default().with_layout(bs::navbar::Layout::SimpleToggle)
}
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
pub fn simple_brand_left(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
pub fn brand_left(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::BrandLeft(Embed::with(brand)))
}
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
pub fn brand_right(brand: bs::navbar::Brand) -> Self {
Self::default().with_layout(bs::navbar::Layout::BrandRight(Embed::with(brand)))
}
pub trait NavbarBootsier {
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
pub fn offcanvas(oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::Offcanvas(Embed::with(oc)))
}
fn offcanvas(oc: bs::Offcanvas) -> Self;
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
pub fn offcanvas_brand_left(brand: bs::navbar::Brand, oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::OffcanvasBrandLeft(
Embed::with(brand),
Embed::with(oc),
))
}
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self;
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
pub fn offcanvas_brand_right(brand: bs::navbar::Brand, oc: bs::Offcanvas) -> Self {
Self::default().with_layout(bs::navbar::Layout::OffcanvasBrandRight(
Embed::with(brand),
Embed::with(oc),
))
}
// **< 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.
///
/// También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`Bg`]).
/// - Definir la apariencia del texto ([`Text`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// 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.
#[builder_fn]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp;
self
}
/// Define el tipo de disposición que tendrá la barra de navegación.
#[builder_fn]
pub fn with_layout(mut self, layout: bs::navbar::Layout) -> Self {
self.layout = layout;
self
}
fn with_expand(self, bp: BreakPoint) -> Self;
/// Define dónde se mostrará la barra de navegación dentro del documento.
#[builder_fn]
pub fn with_position(mut self, position: bs::navbar::Position) -> Self {
self.position = position;
fn with_position(self, position: bs::navbar::Position) -> Self;
}
impl NavbarBootsier for Navbar {
fn offcanvas(oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::Offcanvas(Embed::with(oc)),
));
navbar
}
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), Embed::with(oc)),
));
navbar
}
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), Embed::with(oc)),
));
navbar
}
#[builder_fn]
fn with_expand(mut self, bp: BreakPoint) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp));
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 [`ChildOp`].
///
/// # Ejemplo
///
/// ```rust,ignore
/// navbar.with_item(navbar::Item::nav(...));
/// navbar.with_item(ChildOp::AddMany(vec![
/// navbar::Item::nav(...).into(),
/// navbar::Item::text(...).into(),
/// ]));
/// ```
#[builder_fn]
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
self.items.alter_child(op.into());
fn with_position(mut self, position: bs::navbar::Position) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position));
self
}
}
// **< Navbar SETUP >*******************************************************************************
pub(crate) fn setup(navbar: &mut Navbar) {
// 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);
let position = navbar
.props()
.extra_or(EXTRA_POSITION, bs::navbar::Position::default());
let mut classes = String::new();
expand.push_to(&mut classes, "navbar-expand", "");
position.push_to(&mut classes);
if !classes.is_empty() {
navbar.alter_prop(PropsOp::add_classes(classes));
}
}
// **< Navbar RENDER >******************************************************************************
pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup, ComponentError> {
// Botón de despliegue (colapso u offcanvas) para la barra.
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
let id_content_target = util::join!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false")
} else {
None
};
html! {
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[Lc::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{
span class="navbar-toggler-icon" {}
}
}
}
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
let items = navbar.items().render(cx).await;
if items.is_empty() {
return Ok(html! {});
}
// `Navbar::setup()` (base) garantiza que habrá un `id` antes de renderizar.
let id = navbar.id().unwrap();
// `with_layout()` (extra propio de Bootsier) tiene prioridad; si no se ha usado, se traduce el
// `navbar::Layout` de base que hayan podido fijar los constructores heredados de `Navbar`
// (`simple()`, `brand_left()`...), que Bootsier no puede sobrescribir por nombre -las funciones
// inherentes de base siempre ganan sobre las de un trait con el mismo nombre-.
let layout = navbar
.props()
.extra::<bs::navbar::Layout>(EXTRA_LAYOUT)
.cloned()
.unwrap_or_else(|_| translate_layout(navbar.layout()));
Ok(html! {
nav (navbar.props()) {
div class="container-fluid" {
@match layout {
// Barra más sencilla: sólo contenido.
bs::navbar::Layout::Simple => {
(items)
},
// Barra sencilla que se puede contraer/expandir.
bs::navbar::Layout::SimpleToggle => {
@let id_content = util::join!(&id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con marca a la izquierda, siempre visible.
bs::navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
(items)
},
// Barra con marca a la izquierda y botón a la derecha.
bs::navbar::Layout::BrandLeft(brand) => {
@let id_content = util::join!(&id, "-content");
(brand.render(cx).await)
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con botón a la izquierda y marca a la derecha.
bs::navbar::Layout::BrandRight(brand) => {
@let id_content = util::join!(&id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx).await)
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
bs::navbar::Layout::Offcanvas(offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
// Barra con marca a la izquierda y contenido en offcanvas.
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx).await)
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
// Barra con contenido en offcanvas y marca a la derecha.
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
(brand.render(cx).await)
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
}
}
}
})
}
// Traduce el `navbar::Layout` semántico de base (sin `Offcanvas`, sin `Position`/`expand`) a la
// variante equivalente de `bs::navbar::Layout`, para las barras construidas con los constructores
// heredados de `Navbar` (`simple()`, `simple_toggle()`, `simple_brand_left()`, `brand_left()`,
// `brand_right()`) en vez de con `with_layout()`.
fn translate_layout(layout: &navbar::Layout) -> bs::navbar::Layout {
match layout {
navbar::Layout::Simple => bs::navbar::Layout::Simple,
navbar::Layout::SimpleToggle => bs::navbar::Layout::SimpleToggle,
navbar::Layout::SimpleBrandLeft(brand) => {
bs::navbar::Layout::SimpleBrandLeft(brand.clone())
}
navbar::Layout::BrandLeft(brand) => bs::navbar::Layout::BrandLeft(brand.clone()),
navbar::Layout::BrandRight(brand) => bs::navbar::Layout::BrandRight(brand.clone()),
}
}

View file

@ -1,98 +1,24 @@
use pagetop::prelude::*;
use crate::theme::*;
use crate::theme::bs::nav;
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::bs::Navbar).
///
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
/// independientes de la marca o del botón que ya pueda definir el propio
/// [`navbar::Layout`](crate::theme::bs::navbar::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`](crate::theme::bs::navbar::Layout) no incluye marca, y se
/// quiere incluir dentro del área
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
/// sólo añade otra dentro del bloque de contenidos.
Brand(Embed<bs::navbar::Brand>),
/// Representa un menú de navegación [`Nav`](crate::theme::bs::Nav).
Nav(Embed<bs::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,
// Idéntico a `navbar::Item::prepare()` de base salvo la variante `Nav`: en vez de reconstruir el
// `<ul>` a mano (lo que se salta la cadena de temas), clona el `Nav` embebido, lo marca como
// "dentro de una Navbar" (para que `theme::bs::nav::setup()` use `navbar-nav` en vez de `nav` como
// clase base) y lo renderiza con su ciclo de vida completo -así recibe también `kind`/`layout`
// traducidos a clases de Bootstrap, y cualquier otro tema que intercepte `Nav` en el futuro-.
pub(crate) async fn render(
item: &navbar::Item,
cx: &mut Context,
) -> Result<Markup, ComponentError> {
match item {
navbar::Item::Nav(embed) => {
let Some(mut nav) = embed.get().cloned() else {
return Ok(html! {});
};
nav.alter_prop(PropsOp::set_extra(nav::EXTRA_IN_NAVBAR, true));
Ok(html! { (nav.render(cx).await) })
}
}
fn setup(&mut self, _cx: &Context) {
if let Self::Nav(nav) = self
&& let Some(nav) = nav.get_mut()
{
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) => {
if let Some(nav) = nav.get() {
let items = nav.items().render(cx).await;
if items.is_empty() {
return Ok(html! {});
}
html! {
ul id=[nav.id()] (nav.props()) {
(items)
}
}
} else {
html! {}
}
}
Self::Text(text) => html! {
span class="navbar-text" {
(text.using(cx))
}
},
})
}
}
impl Item {
/// Crea un elemento de tipo [`navbar::Brand`](crate::theme::bs::navbar::Brand) para añadir en el contenido de [`Navbar`](crate::theme::bs::Navbar).
///
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
/// desplegable.
pub fn brand(brand: bs::navbar::Brand) -> Self {
Self::Brand(Embed::with(brand))
}
/// Crea un elemento de tipo [`Nav`](crate::theme::bs::Nav) para añadir al contenido de [`Navbar`](crate::theme::bs::Navbar).
pub fn nav(item: bs::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)
_ => item.prepare(cx).await,
}
}

View file

@ -20,13 +20,13 @@ pub enum Layout {
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
SimpleBrandLeft(Embed<bs::navbar::Brand>),
SimpleBrandLeft(Embed<Brand>),
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
BrandLeft(Embed<bs::navbar::Brand>),
BrandLeft(Embed<Brand>),
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
BrandRight(Embed<bs::navbar::Brand>),
BrandRight(Embed<Brand>),
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
/// izquierda y sin marca de identidad.
@ -34,11 +34,11 @@ pub enum Layout {
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con marca de identidad a la
/// izquierda y botón de despliegue a la derecha.
OffcanvasBrandLeft(Embed<bs::navbar::Brand>, Embed<bs::Offcanvas>),
OffcanvasBrandLeft(Embed<Brand>, Embed<bs::Offcanvas>),
/// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la
/// izquierda y marca de identidad a la derecha.
OffcanvasBrandRight(Embed<bs::navbar::Brand>, Embed<bs::Offcanvas>),
OffcanvasBrandRight(Embed<Brand>, Embed<bs::Offcanvas>),
}
// **< Position >***********************************************************************************