diff --git a/extensions/pagetop-bootsier/build.rs b/extensions/pagetop-bootsier/build.rs index df7a275..a96301c 100644 --- a/extensions/pagetop-bootsier/build.rs +++ b/extensions/pagetop-bootsier/build.rs @@ -13,8 +13,7 @@ fn main() -> std::io::Result<()> { } fn bootstrap_js_files(path: &Path) -> bool { - let bootstrap_js = "bootstrap.bundle.min.js"; // No filtra durante el desarrollo, solo en la compilación "release". env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release" - || path.file_name().is_some_and(|f| f == bootstrap_js) + || path.file_name().is_some_and(|n| n == "bootstrap.min.js") } diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 6df5341..a0e6c70 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -127,7 +127,7 @@ impl Theme for Bootsier { .with_weight(-90), )) .alter_assets(ContextOp::AddJavaScript( - JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js") + JavaScript::defer("/bootsier/js/bootstrap.min.js") .with_version(BOOTSTRAP_VERSION) .with_weight(-90), )); diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index 4c547f6..d34c0a9 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -24,7 +24,7 @@ pub use nav::Nav; // Navbar. pub mod navbar; #[doc(inline)] -pub use navbar::Navbar; +pub use navbar::{Navbar, NavbarToggler}; // Offcanvas. pub mod offcanvas; diff --git a/extensions/pagetop-bootsier/src/theme/dropdown.rs b/extensions/pagetop-bootsier/src/theme/dropdown.rs index ed4cbec..eb00e4c 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown.rs @@ -6,23 +6,6 @@ //! //! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n). -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let dd = Dropdown::new() -//! .with_title(L10n::n("Menu")) -//! .with_button_color(ButtonColor::Background(Color::Secondary)) -//! .with_auto_close(dropdown::AutoClose::ClickableInside) -//! .with_direction(dropdown::Direction::Dropend) -//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es")) -//! .add_item(dropdown::Item::divider()) -//! .add_item(dropdown::Item::header(L10n::n("User session"))) -//! .add_item(dropdown::Item::button(L10n::n("Sign out"))); -//! ``` mod props; pub use props::{AutoClose, Direction, MenuAlign, MenuPosition}; diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index c29ac14..984a3e0 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -17,8 +17,22 @@ use crate::LOCALES_BOOTSIER; /// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el /// resto de propiedades no afectarán a su representación en [`Nav`]. /// -/// Ver ejemplo en el módulo [`dropdown`]. -/// Si no contiene elementos, el componente **no se renderiza**. +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::prelude::*; +/// let dd = Dropdown::new() +/// .with_title(L10n::n("Menu")) +/// .with_button_color(ButtonColor::Background(Color::Secondary)) +/// .with_auto_close(dropdown::AutoClose::ClickableInside) +/// .with_direction(dropdown::Direction::Dropend) +/// .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/")) +/// .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es")) +/// .add_item(dropdown::Item::divider()) +/// .add_item(dropdown::Item::header(L10n::n("User session"))) +/// .add_item(dropdown::Item::button(L10n::n("Sign out"))); +/// ``` #[rustfmt::skip] #[derive(AutoDefault)] pub struct Dropdown { diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs index a13058d..1aed83b 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs @@ -79,7 +79,7 @@ impl Component for Item { } => { let path = path(cx); let current_path = cx.request().map(|request| request.path()); - let is_current = !*disabled && (current_path == Some(path)); + let is_current = !*disabled && current_path.map_or(false, |p| p == path); let mut classes = "dropdown-item".to_string(); if is_current { @@ -274,7 +274,7 @@ impl Item { &self.classes } - /// Devuelve el tipo de elemento representado. + /// Devuelve el tipo de elemento representado por este elemento. pub fn item_kind(&self) -> &ItemKind { &self.item_kind } diff --git a/extensions/pagetop-bootsier/src/theme/nav.rs b/extensions/pagetop-bootsier/src/theme/nav.rs index c74ab3b..b540c32 100644 --- a/extensions/pagetop-bootsier/src/theme/nav.rs +++ b/extensions/pagetop-bootsier/src/theme/nav.rs @@ -6,26 +6,6 @@ //! //! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n). -//! -//! # Ejemplo -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let nav = Nav::tabs() -//! .with_layout(nav::Layout::End) -//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es")) -//! .add_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("Options")) -//! .with_items(TypedOp::AddMany(vec![ -//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action")), -//! Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")), -//! ])), -//! )) -//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#")); -//! ``` mod props; pub use props::{Kind, Layout}; diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index d4cf2c8..34c33b9 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -8,8 +8,25 @@ use crate::prelude::*; /// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite /// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)). /// -/// Ver ejemplo en el módulo [`nav`]. -/// Si no contiene elementos, el componente **no se renderiza**. +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::prelude::*; +/// let nav = Nav::tabs() +/// .with_layout(nav::Layout::End) +/// .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) +/// .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es")) +/// .add_item(nav::Item::dropdown( +/// Dropdown::new() +/// .with_title(L10n::n("Options")) +/// .with_items(TypedOp::AddMany(vec![ +/// Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action")), +/// Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")), +/// ])), +/// )) +/// .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#")); +/// ``` #[rustfmt::skip] #[derive(AutoDefault)] pub struct Nav { diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index bc097e0..63248f8 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -72,7 +72,7 @@ impl Component for Item { ItemKind::Label(label) => PrepareMarkup::With(html! { li id=[self.id()] class=[self.classes().get()] { - span class="nav-link disabled" aria-disabled="true" { + span { (label.using(cx)) } } @@ -86,7 +86,7 @@ impl Component for Item { } => { let path = path(cx); let current_path = cx.request().map(|request| request.path()); - let is_current = !*disabled && (current_path == Some(path)); + let is_current = !*disabled && current_path.map_or(false, |p| p == path); let mut classes = "nav-link".to_string(); if is_current { @@ -250,7 +250,7 @@ impl Item { &self.classes } - /// Devuelve el tipo de elemento representado. + /// Devuelve el tipo de elemento representado por este elemento. pub fn item_kind(&self) -> &ItemKind { &self.item_kind } diff --git a/extensions/pagetop-bootsier/src/theme/navbar.rs b/extensions/pagetop-bootsier/src/theme/navbar.rs index 7b958d7..72a3af3 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar.rs @@ -1,136 +1,11 @@ -//! Definiciones para crear barras de navegación [`Navbar`]. -//! -//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra -//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús -//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n). -//! -//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand)) -//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web. -//! -//! # Ejemplos -//! -//! Barra **simple**, sólo con un menú horizontal: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let navbar = Navbar::simple() -//! .add_item(navbar::Item::nav( -//! Nav::new() -//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about")) -//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact")) -//! )); -//! ``` -//! -//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let navbar = Navbar::simple_toggle() -//! .with_expand(BreakPoint::MD) -//! .add_item(navbar::Item::nav( -//! Nav::new() -//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.example.com")) -//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support")) -//! )); -//! ``` -//! -//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("PageTop")) -//! .with_path(Some(|_| "/")); -//! -//! let navbar = Navbar::brand_left(brand) -//! .add_item(navbar::Item::nav( -//! Nav::new() -//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("Tools")) -//! .add_item(dropdown::Item::link(L10n::n("Generator"), |_| "/tools/gen")) -//! .add_item(dropdown::Item::link(L10n::n("Reports"), |_| "/tools/reports")) -//! )) -//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#")) -//! )); -//! ``` -//! -//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("Intranet")) -//! .with_path(Some(|_| "/")); -//! -//! let navbar = Navbar::brand_right(brand) -//! .with_expand(BreakPoint::LG) -//! .add_item(navbar::Item::nav( -//! Nav::pills() -//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard")) -//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users")) -//! )); -//! ``` -//! -//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let oc = Offcanvas::new() -//! .with_id("main_offcanvas") -//! .with_title(L10n::n("Main menu")) -//! .with_placement(offcanvas::Placement::Start) -//! .with_backdrop(offcanvas::Backdrop::Enabled); -//! -//! let navbar = Navbar::offcanvas(oc) -//! .add_item(navbar::Item::nav( -//! Nav::new() -//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/")) -//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile")) -//! .add_item(nav::Item::dropdown( -//! Dropdown::new() -//! .with_title(L10n::n("More")) -//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings")) -//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help")) -//! )) -//! )); -//! ``` -//! -//! Barra **fija arriba**: -//! -//! ```rust -//! # use pagetop::prelude::*; -//! # use pagetop_bootsier::prelude::*; -//! let brand = navbar::Brand::new() -//! .with_title(L10n::n("Main App")) -//! .with_path(Some(|_| "/")); -//! -//! let navbar = Navbar::brand_left(brand) -//! .with_position(navbar::Position::FixedTop) -//! .add_item(navbar::Item::nav( -//! Nav::new() -//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/")) -//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors")) -//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock")) -//! )); -//! ``` +mod component; +pub use component::{Navbar, NavbarToggler, NavbarType}; -mod props; -pub use props::{Layout, Position}; +mod button_toggler; +pub use button_toggler::ButtonToggler; + +mod content; +pub use content::{Content, ContentType}; mod brand; pub use brand::Brand; - -mod component; -pub use component::Navbar; - -mod item; -pub use item::Item; diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index e969b0e..cfeb45a 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -2,25 +2,16 @@ use pagetop::prelude::*; use crate::prelude::*; -/// Marca de identidad para mostrar en una barra de navegación [`Navbar`]. -/// -/// Representa la identidad del sitio con una imagen, título y eslogan: -/// -/// - Si hay URL ([`with_path()`](Self::with_path)), el bloque completo actúa como enlace. Por -/// defecto enlaza a la raíz del sitio (`/`). -/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título -/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza. -/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. #[rustfmt::skip] #[derive(AutoDefault)] pub struct Brand { - id : AttrId, - image : Typed, - #[default(_code = "L10n::n(&global::SETTINGS.app.name)")] - title : L10n, - slogan: L10n, - #[default(_code = "Some(|_| \"/\")")] - path : Option, + id : AttrId, + #[default(_code = "global::SETTINGS.app.name.to_owned()")] + app_name : String, + slogan : AttrL10n, + logo : Typed, + #[default(_code = "|_| \"/\"")] + home : FnPathByContext, } impl Component for Brand { @@ -33,79 +24,81 @@ impl Component for Brand { } fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { - let image = self.image().render(cx); - let title = self.title().using(cx); - if title.is_empty() && image.is_empty() { - return PrepareMarkup::None; - } - let slogan = self.slogan().using(cx); + let logo = self.logo().render(cx); + let home = self.home()(cx); + let title = &L10n::l("site_home").lookup(cx); PrepareMarkup::With(html! { - @if let Some(path) = self.path() { - a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) } - } @else { - span class="navbar-brand" { (image) (title) (slogan) } + div id=[self.id()] class="branding__container" { + div class="branding__content" { + @if !logo.is_empty() { + a class="branding__logo" href=(home) title=[title] rel="home" { + (logo) + } + } + div class="branding__text" { + a class="branding__name" href=(home) title=[title] rel="home" { + (self.app_name()) + } + @if let Some(slogan) = self.slogan().lookup(cx) { + div class="branding__slogan" { + (slogan) + } + } + } + } } }) } } impl Brand { - // **< Brand BUILDER >************************************************************************** + // Brand BUILDER. - /// Establece el identificador único (`id`) de la marca. #[builder_fn] pub fn with_id(mut self, id: impl AsRef) -> Self { self.id.alter_value(id); self } - /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. #[builder_fn] - pub fn with_image(mut self, image: Option) -> Self { - self.image.alter_component(image); + pub fn with_app_name(mut self, app_name: impl Into) -> Self { + self.app_name = app_name.into(); self } - /// Establece el título de la identidad de marca. - #[builder_fn] - pub fn with_title(mut self, title: L10n) -> Self { - self.title = title; - self - } - - /// Define el eslogan de la marca. #[builder_fn] pub fn with_slogan(mut self, slogan: L10n) -> Self { - self.slogan = slogan; + self.slogan.alter_value(slogan); self } - /// Define la URL de destino. Si es `None`, la marca no será un enlace. #[builder_fn] - pub fn with_path(mut self, path: Option) -> Self { - self.path = path; + pub fn with_logo(mut self, logo: Option) -> Self { + self.logo.alter_component(logo); self } - // **< Brand GETTERS >************************************************************************** - - /// Devuelve la imagen de marca (si la hay). - pub fn image(&self) -> &Typed { - &self.image + #[builder_fn] + pub fn with_home(mut self, home: FnPathByContext) -> Self { + self.home = home; + self } - /// Devuelve el título de la identidad de marca. - pub fn title(&self) -> &L10n { - &self.title + // Brand GETTERS. + + pub fn app_name(&self) -> &String { + &self.app_name } - /// Devuelve el eslogan de la marca. - pub fn slogan(&self) -> &L10n { + pub fn slogan(&self) -> &AttrL10n { &self.slogan } - /// Devuelve la función que resuelve la URL asociada a la marca (si existe). - pub fn path(&self) -> &Option { - &self.path + pub fn logo(&self) -> &Typed { + &self.logo + } + + pub fn home(&self) -> &FnPathByContext { + &self.home } } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/button_toggler.rs b/extensions/pagetop-bootsier/src/theme/navbar/button_toggler.rs new file mode 100644 index 0000000..036182a --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/navbar/button_toggler.rs @@ -0,0 +1,73 @@ +use pagetop::prelude::*; + +use crate::LOCALES_BOOTSIER; + +use std::fmt; + +#[derive(AutoDefault, PartialEq)] +pub(crate) enum Toggle { + #[default] + Collapse, + Offcanvas, +} + +#[rustfmt::skip] +impl fmt::Display for Toggle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Toggle::Collapse => write!(f, "collapse"), + Toggle::Offcanvas => write!(f, "offcanvas"), + } + } +} + +#[derive(AutoDefault)] +pub struct ButtonToggler; + +impl Component for ButtonToggler { + fn new() -> Self { + ButtonToggler::default() + } + + fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(html! { + button + type="button" + class="navbar-toggler" + { + span class="navbar-toggler-icon" {} + } + }) + } +} + +impl ButtonToggler { + // ButtonToggler PRIVATE RENDER. + + pub(crate) fn render( + &self, + cx: &mut Context, + id_content: String, + data_bs_toggle: Toggle, + ) -> Markup { + let id_content_target = 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=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)] + { + span class="navbar-toggler-icon" {} + } + } + } +} diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index fc46175..4a203c3 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -6,23 +6,32 @@ use crate::LOCALES_BOOTSIER; const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_OFFCANVAS: &str = "offcanvas"; -/// Componente para crear una **barra de navegación**. -/// -/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con -/// 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`]. -/// -/// Ver ejemplos en el módulo [`navbar`]. -/// Si no contiene elementos, el componente **no se renderiza**. +#[derive(AutoDefault)] +pub enum NavbarToggler { + #[default] + Enabled, + Disabled, +} + +#[derive(AutoDefault)] +pub enum NavbarType { + #[default] + None, + Nav(Typed), + Offcanvas(Typed), + Text(L10n), +} + #[rustfmt::skip] #[derive(AutoDefault)] pub struct Navbar { - id : AttrId, - classes : AttrClasses, - expand : BreakPoint, - layout : navbar::Layout, - position: navbar::Position, - items : Children, + id : AttrId, + classes : AttrClasses, + expand : BreakPoint, + toggler : NavbarToggler, + navbar_type: NavbarType, + contents : Children, + brand : Typed, } impl Component for Navbar { @@ -40,22 +49,162 @@ impl Component for Navbar { [ "navbar".to_string(), self.expand().try_class("navbar-expand").unwrap_or_default(), - match self.position() { - navbar::Position::Static => "", - navbar::Position::FixedTop => "fixed-top", - navbar::Position::FixedBottom => "fixed-bottom", - navbar::Position::StickyTop => "sticky-top", - navbar::Position::StickyBottom => "sticky-bottom", - } - .to_string(), ] .join(" "), ); } fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { - // 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 = cx.required_id::(self.id()); + + let navbar_type = match self.navbar_type() { + NavbarType::None => return PrepareMarkup::None, + NavbarType::Nav(nav) => { + let id_content = join!(id, "-content"); + match self.toggler() { + NavbarToggler::Enabled => self.toggler_wrapper( + TOGGLE_COLLAPSE, + L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx), + id_content, + self.brand().render(cx), + nav.render(cx), + ), + NavbarToggler::Disabled => nav.render(cx), + } + } + NavbarType::Offcanvas(oc) => { + let id_content = oc.id().unwrap_or_default(); + self.toggler_wrapper( + TOGGLE_OFFCANVAS, + L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx), + id_content, + self.brand().render(cx), + oc.render(cx), + ) + } + NavbarType::Text(text) => html! { + span class="navbar-text" { + (text.using(cx)) + } + }, + }; + + self.nav_wrapper(id, self.brand().render(cx), navbar_type) + } +} + +impl Navbar { + pub fn with_nav(nav: navbar::Nav) -> Self { + Navbar::default().with_navbar_type(NavbarType::Nav(Typed::with(nav))) + } + + pub fn with_offcanvas(offcanvas: Offcanvas) -> Self { + Navbar::default().with_navbar_type(NavbarType::Offcanvas(Typed::with(offcanvas))) + } + + // Navbar BUILDER. + + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_value(id); + self + } + + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_value(op, classes); + self + } + + #[builder_fn] + pub fn with_expand(mut self, bp: BreakPoint) -> Self { + self.expand = bp; + self + } + + #[builder_fn] + pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self { + self.toggler = toggler; + self + } + + #[builder_fn] + pub fn with_navbar_type(mut self, navbar_type: NavbarType) -> Self { + self.navbar_type = navbar_type; + self + } + + pub fn with_content(mut self, content: navbar::Content) -> Self { + self.contents.add(Child::with(content)); + self + } + + #[builder_fn] + pub fn with_contents(mut self, op: TypedOp) -> Self { + self.contents.alter_typed(op); + self + } + + #[builder_fn] + pub fn with_brand(mut self, brand: Option) -> Self { + self.brand.alter_component(brand); + self + } + + // Navbar GETTERS. + + pub fn classes(&self) -> &AttrClasses { + &self.classes + } + + pub fn expand(&self) -> &BreakPoint { + &self.expand + } + + pub fn toggler(&self) -> &NavbarToggler { + &self.toggler + } + + pub fn navbar_type(&self) -> &NavbarType { + &self.navbar_type + } + + pub fn contents(&self) -> &Children { + &self.contents + } + + pub fn brand(&self) -> &Typed { + &self.brand + } + + // Navbar HELPERS. + + fn nav_wrapper(&self, id: String, brand: Markup, content: Markup) -> PrepareMarkup { + if content.is_empty() { + PrepareMarkup::None + } else { + PrepareMarkup::With(html! { + (brand) + nav id=(id) class=[self.classes().get()] { + div class="container-fluid" { + (content) + } + } + }) + } + } + + fn toggler_wrapper( + &self, + data_bs_toggle: &str, + aria_label: Option, + id_content: String, + brand: Markup, + content: Markup, + ) -> Markup { + if content.is_empty() { + html! {} + } else { let id_content_target = join!("#", id_content); let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE { Some("false") @@ -63,6 +212,7 @@ impl Component for Navbar { None }; html! { + (brand) button type="button" class="navbar-toggler" @@ -70,229 +220,14 @@ impl Component for Navbar { data-bs-target=(id_content_target) aria-controls=(id_content) aria-expanded=[aria_expanded] - aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)] + aria-label=[aria_label] { span class="navbar-toggler-icon" {} } - } - } - - // Si no hay contenidos, no tiene sentido mostrar una barra vacía. - let items = self.items().render(cx); - if items.is_empty() { - return PrepareMarkup::None; - } - - // Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas. - let id = cx.required_id::(self.id()); - - PrepareMarkup::With(html! { - nav id=(id) class=[self.classes().get()] { - div class="container-fluid" { - @match self.layout() { - // Barra más sencilla: sólo contenido. - navbar::Layout::Simple => { - (items) - }, - - // Barra sencilla que se puede contraer/expandir. - navbar::Layout::SimpleToggle => { - @let id_content = 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. - navbar::Layout::SimpleBrandLeft(brand) => { - (brand.render(cx)) - (items) - }, - - // Barra con marca a la izquierda y botón a la derecha. - navbar::Layout::BrandLeft(brand) => { - @let id_content = join!(id, "-content"); - - (brand.render(cx)) - (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. - navbar::Layout::BrandRight(brand) => { - @let id_content = join!(id, "-content"); - - (button(cx, TOGGLE_COLLAPSE, &id_content)) - (brand.render(cx)) - div id=(id_content) class="collapse navbar-collapse" { - (items) - } - }, - - // Barra cuyo contenido se muestra en un offcanvas, sin marca. - navbar::Layout::Offcanvas(offcanvas) => { - @let id_content = offcanvas.id().unwrap_or_default(); - - (button(cx, TOGGLE_OFFCANVAS, &id_content)) - @if let Some(oc) = offcanvas.borrow() { - (oc.render_offcanvas(cx, Some(self.items()))) - } - }, - - // Barra con marca a la izquierda y contenido en offcanvas. - navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => { - @let id_content = offcanvas.id().unwrap_or_default(); - - (brand.render(cx)) - (button(cx, TOGGLE_OFFCANVAS, &id_content)) - @if let Some(oc) = offcanvas.borrow() { - (oc.render_offcanvas(cx, Some(self.items()))) - } - }, - - // Barra con contenido en offcanvas y marca a la derecha. - navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => { - @let id_content = offcanvas.id().unwrap_or_default(); - - (button(cx, TOGGLE_OFFCANVAS, &id_content)) - (brand.render(cx)) - @if let Some(oc) = offcanvas.borrow() { - (oc.render_offcanvas(cx, Some(self.items()))) - } - }, - } + div id=(id_content) class="collapse navbar-collapse" { + (content) } } - }) - } -} - -impl Navbar { - /// Crea una barra de navegación **simple**, sin marca y sin botón. - pub fn simple() -> Self { - Navbar::default().with_layout(navbar::Layout::Simple) - } - - /// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda. - pub fn simple_toggle() -> Self { - Navbar::default().with_layout(navbar::Layout::SimpleToggle) - } - - /// Crea una barra de navegación **con marca a la izquierda**, siempre visible. - pub fn simple_brand_left(brand: navbar::Brand) -> Self { - Navbar::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand))) - } - - /// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**. - pub fn brand_left(brand: navbar::Brand) -> Self { - Navbar::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand))) - } - - /// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**. - pub fn brand_right(brand: navbar::Brand) -> Self { - Navbar::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand))) - } - - /// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**. - pub fn offcanvas(oc: Offcanvas) -> Self { - Navbar::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc))) - } - - /// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**. - pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self { - Navbar::default().with_layout(navbar::Layout::OffcanvasBrandLeft( - Typed::with(brand), - Typed::with(oc), - )) - } - - /// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**. - pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self { - Navbar::default().with_layout(navbar::Layout::OffcanvasBrandRight( - Typed::with(brand), - Typed::with(oc), - )) - } - - // **< Navbar BUILDER >************************************************************************* - - /// Establece el identificador único (`id`) de la barra de navegación. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_value(id); - self - } - - /// Modifica la lista de clases CSS aplicadas a la barra de navegación. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_value(op, classes); - 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: navbar::Layout) -> Self { - self.layout = layout; - self - } - - /// Define dónde se mostrará la barra de navegación dentro del documento. - #[builder_fn] - pub fn with_position(mut self, position: navbar::Position) -> Self { - self.position = position; - self - } - - /// Añade un nuevo contenido hijo. - #[inline] - pub fn add_item(mut self, item: navbar::Item) -> Self { - self.items.add(Child::with(item)); - self - } - - /// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`]. - #[builder_fn] - pub fn with_items(mut self, op: TypedOp) -> Self { - self.items.alter_typed(op); - self - } - - // **< Navbar GETTERS >************************************************************************* - - /// Devuelve las clases CSS asociadas a la barra de navegación. - pub fn classes(&self) -> &AttrClasses { - &self.classes - } - - /// Devuelve el punto de ruptura configurado. - pub fn expand(&self) -> &BreakPoint { - &self.expand - } - - /// Devuelve la disposición configurada para la barra de navegación. - pub fn layout(&self) -> &navbar::Layout { - &self.layout - } - - /// Devuelve la posición configurada para la barra de navegación. - pub fn position(&self) -> &navbar::Position { - &self.position - } - - /// Devuelve la lista de contenidos (`children`). - pub fn items(&self) -> &Children { - &self.items + } } } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/content.rs b/extensions/pagetop-bootsier/src/theme/navbar/content.rs new file mode 100644 index 0000000..2efb4a0 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/navbar/content.rs @@ -0,0 +1,69 @@ +use pagetop::prelude::*; + +use crate::theme::navbar; + +#[derive(AutoDefault)] +pub enum ContentType { + #[default] + None, + Brand(Typed), + Nav(Typed), + Text(L10n), +} + +// Item. + +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct Content { + content: ContentType, +} + +impl Component for Content { + fn new() -> Self { + Content::default() + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + match self.content() { + ContentType::None => PrepareMarkup::None, + ContentType::Brand(brand) => PrepareMarkup::With(html! { + (brand.render(cx)) + }), + ContentType::Nav(nav) => PrepareMarkup::With(html! { + (nav.render(cx)) + }), + ContentType::Text(text) => PrepareMarkup::With(html! { + span class="navbar-text" { + (text.using(cx)) + } + }), + } + } +} + +impl Content { + pub fn brand(content: navbar::Brand) -> Self { + Content { + content: ContentType::Brand(Typed::with(content)), + } + } + + pub fn nav(content: navbar::Nav) -> Self { + Content { + content: ContentType::Nav(Typed::with(content)), + } + } + + pub fn text(content: L10n) -> Self { + Content { + content: ContentType::Text(content), + } + } + + // Content GETTERS. + + pub fn content(&self) -> &ContentType { + &self.content + } +} diff --git a/extensions/pagetop-bootsier/src/theme/navbar/item.rs b/extensions/pagetop-bootsier/src/theme/navbar/item.rs deleted file mode 100644 index f064640..0000000 --- a/extensions/pagetop-bootsier/src/theme/navbar/item.rs +++ /dev/null @@ -1,108 +0,0 @@ -use pagetop::prelude::*; - -use crate::prelude::*; - -/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::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`]. -#[derive(AutoDefault)] -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`] 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(Typed), - /// Representa un menú de navegación [`Nav`](crate::theme::Nav). - Nav(Typed