Compare commits

..

2 commits

Author SHA1 Message Date
93804721e1 📝 Repasa doc de Dropdown, Nav y Offcanvas 2025-11-02 12:42:36 +01:00
749e182619 [bootsier] Añade componente Navbar 2025-11-02 12:40:26 +01:00
22 changed files with 739 additions and 495 deletions

View file

@ -13,7 +13,8 @@ fn main() -> std::io::Result<()> {
} }
fn bootstrap_js_files(path: &Path) -> bool { 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". // No filtra durante el desarrollo, solo en la compilación "release".
env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release" env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release"
|| path.file_name().is_some_and(|n| n == "bootstrap.min.js") || path.file_name().is_some_and(|f| f == bootstrap_js)
} }

View file

@ -127,7 +127,7 @@ impl Theme for Bootsier {
.with_weight(-90), .with_weight(-90),
)) ))
.alter_assets(ContextOp::AddJavaScript( .alter_assets(ContextOp::AddJavaScript(
JavaScript::defer("/bootsier/js/bootstrap.min.js") JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
.with_version(BOOTSTRAP_VERSION) .with_version(BOOTSTRAP_VERSION)
.with_weight(-90), .with_weight(-90),
)); ));

View file

@ -24,7 +24,7 @@ pub use nav::Nav;
// Navbar. // Navbar.
pub mod navbar; pub mod navbar;
#[doc(inline)] #[doc(inline)]
pub use navbar::{Navbar, NavbarToggler}; pub use navbar::Navbar;
// Offcanvas. // Offcanvas.
pub mod offcanvas; pub mod offcanvas;

View file

@ -6,6 +6,23 @@
//! //!
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! 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). //! 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; mod props;
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition}; pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};

View file

@ -17,22 +17,8 @@ use crate::LOCALES_BOOTSIER;
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el /// 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`]. /// resto de propiedades no afectarán a su representación en [`Nav`].
/// ///
/// # Ejemplo /// Ver ejemplo en el módulo [`dropdown`].
/// /// Si no contiene elementos, el componente **no se renderiza**.
/// ```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] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Dropdown { pub struct Dropdown {

View file

@ -79,7 +79,7 @@ impl Component for Item {
} => { } => {
let path = path(cx); let path = path(cx);
let current_path = cx.request().map(|request| request.path()); let current_path = cx.request().map(|request| request.path());
let is_current = !*disabled && current_path.map_or(false, |p| p == path); let is_current = !*disabled && (current_path == Some(path));
let mut classes = "dropdown-item".to_string(); let mut classes = "dropdown-item".to_string();
if is_current { if is_current {
@ -274,7 +274,7 @@ impl Item {
&self.classes &self.classes
} }
/// Devuelve el tipo de elemento representado por este elemento. /// Devuelve el tipo de elemento representado.
pub fn item_kind(&self) -> &ItemKind { pub fn item_kind(&self) -> &ItemKind {
&self.item_kind &self.item_kind
} }

View file

@ -6,6 +6,26 @@
//! //!
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y //! 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). //! 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; mod props;
pub use props::{Kind, Layout}; pub use props::{Kind, Layout};

View file

@ -8,25 +8,8 @@ use crate::prelude::*;
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite /// 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)). /// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
/// ///
/// # Ejemplo /// Ver ejemplo en el módulo [`nav`].
/// /// Si no contiene elementos, el componente **no se renderiza**.
/// ```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] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Nav { pub struct Nav {

View file

@ -72,7 +72,7 @@ impl Component for Item {
ItemKind::Label(label) => PrepareMarkup::With(html! { ItemKind::Label(label) => PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] { li id=[self.id()] class=[self.classes().get()] {
span { span class="nav-link disabled" aria-disabled="true" {
(label.using(cx)) (label.using(cx))
} }
} }
@ -86,7 +86,7 @@ impl Component for Item {
} => { } => {
let path = path(cx); let path = path(cx);
let current_path = cx.request().map(|request| request.path()); let current_path = cx.request().map(|request| request.path());
let is_current = !*disabled && current_path.map_or(false, |p| p == path); let is_current = !*disabled && (current_path == Some(path));
let mut classes = "nav-link".to_string(); let mut classes = "nav-link".to_string();
if is_current { if is_current {
@ -250,7 +250,7 @@ impl Item {
&self.classes &self.classes
} }
/// Devuelve el tipo de elemento representado por este elemento. /// Devuelve el tipo de elemento representado.
pub fn item_kind(&self) -> &ItemKind { pub fn item_kind(&self) -> &ItemKind {
&self.item_kind &self.item_kind
} }

View file

@ -1,11 +1,136 @@
mod component; //! Definiciones para crear barras de navegación [`Navbar`].
pub use component::{Navbar, NavbarToggler, NavbarType}; //!
//! 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 button_toggler; mod props;
pub use button_toggler::ButtonToggler; pub use props::{Layout, Position};
mod content;
pub use content::{Content, ContentType};
mod brand; mod brand;
pub use brand::Brand; pub use brand::Brand;
mod component;
pub use component::Navbar;
mod item;
pub use item::Item;

View file

@ -2,16 +2,25 @@ use pagetop::prelude::*;
use crate::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] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Brand { pub struct Brand {
id : AttrId, id : AttrId,
#[default(_code = "global::SETTINGS.app.name.to_owned()")] image : Typed<Image>,
app_name : String, #[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
slogan : AttrL10n, title : L10n,
logo : Typed<Image>, slogan: L10n,
#[default(_code = "|_| \"/\"")] #[default(_code = "Some(|_| \"/\")")]
home : FnPathByContext, path : Option<FnPathByContext>,
} }
impl Component for Brand { impl Component for Brand {
@ -24,81 +33,79 @@ impl Component for Brand {
} }
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let logo = self.logo().render(cx); let image = self.image().render(cx);
let home = self.home()(cx); let title = self.title().using(cx);
let title = &L10n::l("site_home").lookup(cx); if title.is_empty() && image.is_empty() {
return PrepareMarkup::None;
}
let slogan = self.slogan().using(cx);
PrepareMarkup::With(html! { PrepareMarkup::With(html! {
div id=[self.id()] class="branding__container" { @if let Some(path) = self.path() {
div class="branding__content" { a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) }
@if !logo.is_empty() { } @else {
a class="branding__logo" href=(home) title=[title] rel="home" { span class="navbar-brand" { (image) (title) (slogan) }
(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 { impl Brand {
// Brand BUILDER. // **< Brand BUILDER >**************************************************************************
/// Establece el identificador único (`id`) de la marca.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id); self.id.alter_value(id);
self self
} }
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
#[builder_fn] #[builder_fn]
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self { pub fn with_image(mut self, image: Option<Image>) -> Self {
self.app_name = app_name.into(); self.image.alter_component(image);
self 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] #[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self { pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan.alter_value(slogan); self.slogan = slogan;
self self
} }
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
#[builder_fn] #[builder_fn]
pub fn with_logo(mut self, logo: Option<Image>) -> Self { pub fn with_path(mut self, path: Option<FnPathByContext>) -> Self {
self.logo.alter_component(logo); self.path = path;
self self
} }
#[builder_fn] // **< Brand GETTERS >**************************************************************************
pub fn with_home(mut self, home: FnPathByContext) -> Self {
self.home = home; /// Devuelve la imagen de marca (si la hay).
self pub fn image(&self) -> &Typed<Image> {
&self.image
} }
// Brand GETTERS. /// Devuelve el título de la identidad de marca.
pub fn title(&self) -> &L10n {
pub fn app_name(&self) -> &String { &self.title
&self.app_name
} }
pub fn slogan(&self) -> &AttrL10n { /// Devuelve el eslogan de la marca.
pub fn slogan(&self) -> &L10n {
&self.slogan &self.slogan
} }
pub fn logo(&self) -> &Typed<Image> { /// Devuelve la función que resuelve la URL asociada a la marca (si existe).
&self.logo pub fn path(&self) -> &Option<FnPathByContext> {
} &self.path
pub fn home(&self) -> &FnPathByContext {
&self.home
} }
} }

View file

@ -1,73 +0,0 @@
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" {}
}
}
}
}

View file

@ -6,32 +6,23 @@ use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas"; const TOGGLE_OFFCANVAS: &str = "offcanvas";
#[derive(AutoDefault)] /// Componente para crear una **barra de navegación**.
pub enum NavbarToggler { ///
#[default] /// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
Enabled, /// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
Disabled, /// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
} ///
/// Ver ejemplos en el módulo [`navbar`].
#[derive(AutoDefault)] /// Si no contiene elementos, el componente **no se renderiza**.
pub enum NavbarType {
#[default]
None,
Nav(Typed<navbar::Nav>),
Offcanvas(Typed<Offcanvas>),
Text(L10n),
}
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Navbar { pub struct Navbar {
id : AttrId, id : AttrId,
classes : AttrClasses, classes : AttrClasses,
expand : BreakPoint, expand : BreakPoint,
toggler : NavbarToggler, layout : navbar::Layout,
navbar_type: NavbarType, position: navbar::Position,
contents : Children, items : Children,
brand : Typed<navbar::Brand>,
} }
impl Component for Navbar { impl Component for Navbar {
@ -49,162 +40,22 @@ impl Component for Navbar {
[ [
"navbar".to_string(), "navbar".to_string(),
self.expand().try_class("navbar-expand").unwrap_or_default(), 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(" "), .join(" "),
); );
} }
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let id = cx.required_id::<Self>(self.id()); // Botón de despliegue (colapso u offcanvas) para la barra.
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
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<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> 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<navbar::Content>) -> Self {
self.contents.alter_typed(op);
self
}
#[builder_fn]
pub fn with_brand(mut self, brand: Option<navbar::Brand>) -> 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<navbar::Brand> {
&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<String>,
id_content: String,
brand: Markup,
content: Markup,
) -> Markup {
if content.is_empty() {
html! {}
} else {
let id_content_target = join!("#", id_content); let id_content_target = join!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE { let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false") Some("false")
@ -212,7 +63,6 @@ impl Navbar {
None None
}; };
html! { html! {
(brand)
button button
type="button" type="button"
class="navbar-toggler" class="navbar-toggler"
@ -220,14 +70,229 @@ impl Navbar {
data-bs-target=(id_content_target) data-bs-target=(id_content_target)
aria-controls=(id_content) aria-controls=(id_content)
aria-expanded=[aria_expanded] aria-expanded=[aria_expanded]
aria-label=[aria_label] aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{ {
span class="navbar-toggler-icon" {} 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>(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" { div id=(id_content) class="collapse navbar-collapse" {
(content) (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())))
}
},
} }
} }
} }
})
}
}
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<str>) -> 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<str>) -> 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<navbar::Item>) -> 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
} }
} }

View file

@ -1,69 +0,0 @@
use pagetop::prelude::*;
use crate::theme::navbar;
#[derive(AutoDefault)]
pub enum ContentType {
#[default]
None,
Brand(Typed<navbar::Brand>),
Nav(Typed<navbar::Nav>),
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
}
}

View file

@ -0,0 +1,108 @@
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<navbar::Brand>),
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
Nav(Typed<Nav>),
/// Representa un texto libre localizado.
Text(L10n),
}
impl Component for Item {
fn new() -> Self {
Item::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 prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self {
Self::Void => PrepareMarkup::None,
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
Self::Nav(nav) => {
if let Some(nav) = nav.borrow() {
let items = nav.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
let classes = AttrClasses::new(
[
"navbar-nav",
match nav.nav_kind() {
nav::Kind::Default => "",
nav::Kind::Tabs => "nav-tabs",
nav::Kind::Pills => "nav-pills",
nav::Kind::Underline => "nav-underline",
},
match nav.nav_layout() {
nav::Layout::Default => "",
nav::Layout::Start => "justify-content-start",
nav::Layout::Center => "justify-content-center",
nav::Layout::End => "justify-content-end",
nav::Layout::Vertical => "flex-column",
nav::Layout::Fill => "nav-fill",
nav::Layout::Justified => "nav-justified",
},
]
.join(" "),
);
PrepareMarkup::With(html! {
ul id=[nav.id()] class=[classes.get()] {
(items)
}
})
} else {
PrepareMarkup::None
}
}
Self::Text(text) => PrepareMarkup::With(html! {
span class="navbar-text" {
(text.using(cx))
}
}),
}
}
}
impl Item {
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
///
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
/// desplegable.
pub fn brand(brand: navbar::Brand) -> Self {
Self::Brand(Typed::with(brand))
}
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
pub fn nav(item: Nav) -> Self {
Self::Nav(Typed::with(item))
}
/// Crea un elemento de texto localizado, mostrado sin interacción.
pub fn text(item: L10n) -> Self {
Self::Text(item)
}
}

View file

@ -0,0 +1,64 @@
use pagetop::prelude::*;
use crate::prelude::*;
// **< Layout >*************************************************************************************
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
#[derive(AutoDefault)]
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 a la izquierda y sin marca de identidad.
SimpleToggle,
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
SimpleBrandLeft(Typed<navbar::Brand>),
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
BrandLeft(Typed<navbar::Brand>),
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
BrandRight(Typed<navbar::Brand>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
Offcanvas(Typed<Offcanvas>),
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
/// derecha.
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
/// derecha.
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
}
// **< Position >***********************************************************************************
/// Posición global de una barra de navegación [`Navbar`] en el documento.
#[derive(AutoDefault)]
pub enum Position {
/// Barra normal, fluye con el documento.
#[default]
Static,
/// Barra fijada en la parte superior, siempre visible.
///
/// Puede ser necesario reservar espacio en la parte superior del contenido que fluye debajo
/// para evitar que quede oculto por la barra.
FixedTop,
/// Barra fijada en la parte inferior, siempre visible.
///
/// Puede ser necesario reservar espacio en la parte inferior del contenido que fluye debajo
/// para evitar que quede oculto por la barra.
FixedBottom,
/// La barra de navegación se fija en la parte superior al hacer *scroll*.
StickyTop,
/// La barra de navegación se fija en la parte inferior al hacer *scroll*.
StickyBottom,
}

View file

@ -1,4 +1,24 @@
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`]. //! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
//!
//! # Ejemplo
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let panel = Offcanvas::new()
//! .with_id("offcanvas_example")
//! .with_title(L10n::n("Offcanvas title"))
//! .with_placement(offcanvas::Placement::End)
//! .with_backdrop(offcanvas::Backdrop::Enabled)
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
//! .with_visibility(offcanvas::Visibility::Default)
//! .add_child(Dropdown::new()
//! .with_button_title(L10n::n("Menu"))
//! .add_item(dropdown::Item::label(L10n::n("Label")))
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
//! );
//! ```
mod props; mod props;
pub use props::{Backdrop, BodyScroll, Placement, Visibility}; pub use props::{Backdrop, BodyScroll, Placement, Visibility};

View file

@ -18,27 +18,9 @@ use crate::LOCALES_BOOTSIER;
/// ([`with_breakpoint()`](Self::with_breakpoint)). /// ([`with_breakpoint()`](Self::with_breakpoint)).
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos /// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
/// adecuados para lectores de pantalla y navegación por teclado. /// adecuados para lectores de pantalla y navegación por teclado.
/// - **No se renderiza** si no tiene contenido.
/// ///
/// # Ejemplo /// Ver ejemplo en el módulo [`offcanvas`].
/// /// Si no contiene elementos, el componente **no se renderiza**.
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let panel = Offcanvas::new()
/// .with_id("offcanvas_example")
/// .with_title(L10n::n("Offcanvas title"))
/// .with_placement(offcanvas::Placement::End)
/// .with_backdrop(offcanvas::Backdrop::Enabled)
/// .with_body_scroll(offcanvas::BodyScroll::Enabled)
/// .with_visibility(offcanvas::Visibility::Default)
/// .add_child(Dropdown::new()
/// .with_button_title(L10n::n("Menu"))
/// .add_item(dropdown::Item::label(L10n::n("Label")))
/// .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
/// .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
/// );
/// ```
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Offcanvas { pub struct Offcanvas {
@ -84,54 +66,7 @@ impl Component for Offcanvas {
} }
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let body = self.children().render(cx); PrepareMarkup::With(self.render_offcanvas(cx, None))
if body.is_empty() {
return PrepareMarkup::None;
}
let id = cx.required_id::<Self>(self.id());
let id_label = join!(id, "-label");
let id_target = join!("#", id);
let body_scroll = match self.body_scroll() {
offcanvas::BodyScroll::Disabled => None,
offcanvas::BodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
offcanvas::Backdrop::Disabled => Some("false"),
offcanvas::Backdrop::Enabled => None,
offcanvas::Backdrop::Static => Some("static"),
};
let title = self.title().using(cx);
PrepareMarkup::With(html! {
div
id=(id)
class=[self.classes().get()]
tabindex="-1"
data-bs-scroll=[body_scroll]
data-bs-backdrop=[backdrop]
aria-labelledby=(id_label)
{
div class="offcanvas-header" {
@if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) }
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
{}
}
div class="offcanvas-body" {
(body)
}
}
})
} }
} }
@ -258,4 +193,59 @@ impl Offcanvas {
pub fn children(&self) -> &Children { pub fn children(&self) -> &Children {
&self.children &self.children
} }
// **< Offcanvas HELPERS >**********************************************************************
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
let body = self.children().render(cx);
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {});
if body.is_empty() && body_extra.is_empty() {
return html! {};
}
let id = cx.required_id::<Self>(self.id());
let id_label = join!(id, "-label");
let id_target = join!("#", id);
let body_scroll = match self.body_scroll() {
offcanvas::BodyScroll::Disabled => None,
offcanvas::BodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
offcanvas::Backdrop::Disabled => Some("false"),
offcanvas::Backdrop::Enabled => None,
offcanvas::Backdrop::Static => Some("static"),
};
let title = self.title().using(cx);
html! {
div
id=(id)
class=[self.classes().get()]
tabindex="-1"
data-bs-scroll=[body_scroll]
data-bs-backdrop=[backdrop]
aria-labelledby=(id_label)
{
div class="offcanvas-header" {
@if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) }
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
{}
}
div class="offcanvas-body" {
(body)
(body_extra)
}
}
}
}
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long