From 5bf28ef4f04375b4280ea3a452158cf8deea9983 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 18 Sep 2026 18:40:59 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(bootsier):=20Sustituye=20?= =?UTF-8?q?BreakPoint=20por=20Breakpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elimina el enum BreakPoint propio de Bootsier. Los componentes pasan a usar el Breakpoint del núcleo, cuyo nombre y ancho resuelve el tema activo. --- extensions/pagetop-bootsier/src/lib.rs | 23 +++++- extensions/pagetop-bootsier/src/theme.rs | 49 ++++++++++-- .../src/theme/bs/container.rs | 24 +++--- .../pagetop-bootsier/src/theme/bs/dropdown.rs | 2 +- .../src/theme/bs/dropdown/props.rs | 34 +++++---- .../src/theme/bs/navbar/component.rs | 14 ++-- .../src/theme/bs/offcanvas/component.rs | 12 +-- .../pagetop-bootsier/src/theme/token.rs | 3 - .../src/theme/token/breakpoint.rs | 61 --------------- .../tests/breakpoint_class.rs | 76 +++++++++++++++++++ 10 files changed, 185 insertions(+), 113 deletions(-) delete mode 100644 extensions/pagetop-bootsier/src/theme/token/breakpoint.rs create mode 100644 extensions/pagetop-bootsier/tests/breakpoint_class.rs diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 7991078d..0b948e20 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -125,21 +125,38 @@ impl Extension for Bootsier { #[async_trait] impl Theme for Bootsier { + #[rustfmt::skip] + fn breakpoint_entry(&self, bp: Breakpoint) -> BreakpointEntry { + use Breakpoint::*; + + // Anchos mínimos de la rejilla de Bootstrap (`$grid-breakpoints`). Bootstrap no tiene un + // punto de corte por encima de `xxl`, así que `Xxxl` se resuelve como `xxl`. + match bp { + Xs => BreakpointEntry { breakpoint: Xs, name: "xs", min_width: "" }, + Sm => BreakpointEntry { breakpoint: Sm, name: "sm", min_width: "576px" }, + Md => BreakpointEntry { breakpoint: Md, name: "md", min_width: "768px" }, + Lg => BreakpointEntry { breakpoint: Lg, name: "lg", min_width: "992px" }, + Xl => BreakpointEntry { breakpoint: Xl, name: "xl", min_width: "1200px" }, + Xxl => BreakpointEntry { breakpoint: Xxl, name: "xxl", min_width: "1400px" }, + Xxxl => BreakpointEntry { breakpoint: Xxxl, name: "xxl", min_width: "1400px" }, + } + } + fn intent_color(&self, intent: Intent) -> &'static str { theme::BootsierColors::from(intent).as_str() } - fn setup_component(&self, component: &mut dyn Component, _cx: &mut Context) { + fn setup_component(&self, component: &mut dyn Component, cx: &mut Context) { setup_component!(component, { Badge => |c| theme::bs::badge::setup(c), Brand => |c| theme::bs::brand::setup(c), Button => |c| theme::bs::button::setup(c), - Container => |c| theme::bs::container::setup(c), + Container => |c| theme::bs::container::setup(c, cx), Dialog => |c| theme::bs::dialog::setup(c), Dropdown => |c| theme::bs::dropdown::setup(c), Image => |c| theme::bs::image::setup(c), Nav => |c| theme::bs::nav::setup(c), - Navbar => |c| theme::bs::navbar::setup(c), + Navbar => |c| theme::bs::navbar::setup(c, cx), form::input::Field => |c| theme::bs::form::input::setup(c), form::select::Field => |c| theme::bs::form::select::setup(c), form::Textarea => |c| theme::bs::form::textarea::setup(c), diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index 18b15f8a..f093ac42 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -11,13 +11,10 @@ //! //! # Plantillas //! -//! Bootsier maqueta las dos plantillas de PageTop -//! ([`CoreTemplates`](pagetop::prelude::CoreTemplates)): `Standard`, con -//! cabecera, contenido y pie, que es la plantilla por defecto de cualquier página; y `Admin`, con -//! la shell completa de AdminLTE 4 (barra superior + barra lateral + área de contenido), que se -//! activa creando la página con [`Page::admin()`](pagetop::response::Page::admin) en lugar de -//! [`Page::new()`](pagetop::response::Page::new). No define sus propias variantes de plantilla: -//! intercepta el componente `Template` en `render_component()` (ver `bs::layout`). +//! Bootsier maqueta las dos plantillas de PageTop ([`CoreTemplates`]): `Standard`, con cabecera, +//! contenido y pie, que es la plantilla por defecto de cualquier página; y `Admin`, con la shell +//! completa de administración (barra superior + barra lateral + área de contenido), que se activa +//! creando la página con [`Page::admin()`] en lugar de [`Page::new()`]. //! //! ```rust,no_run //! use pagetop::prelude::*; @@ -32,6 +29,10 @@ //! } //! ``` //! +//! [`CoreTemplates`]: pagetop::prelude::CoreTemplates +//! [`Page::admin()`]: pagetop::response::Page::admin +//! [`Page::new()`]: pagetop::response::Page::new +//! //! # Barra lateral //! //! Registra elementos en [`BootsierRegions::Sidebar`](bs::BootsierRegions::Sidebar) para poblar la @@ -88,6 +89,8 @@ //! })); //! ``` +use pagetop::prelude::*; + pub mod bs; pub mod class; @@ -113,3 +116,35 @@ pub use bs::form::textarea::TextareaBootsier; pub use bs::nav::NavBootsier; #[doc(hidden)] pub use bs::navbar::NavbarBootsier; + +// Añade la clase de un punto de corte con un prefijo y un sufijo (opcional) a la cadena de clases, +// separada con un espacio de las que ya hubiera: `prefix-name-suffix`, sin `-suffix` si no hay +// sufijo, y sin `-name` si el punto de corte aplica siempre. +// +// `name` es el nombre del punto de corte según `Theme::breakpoint_entry()` en el tema activo de +// `cx`. Si el tema no le asocia un ancho mínimo (`Breakpoint::resolved()` devuelve `None`), el +// punto de corte aplica siempre y se omite `name`, igual que Bootstrap con `container` frente a +// `container-lg`, o `container-fluid` frente a `container-lg-fluid`. +pub(crate) fn push_breakpoint_class( + cx: &Context, + bp: Breakpoint, + classes: &mut String, + prefix: &str, + suffix: &str, +) { + if prefix.is_empty() { + return; + } + if !classes.is_empty() { + classes.push(' '); + } + classes.push_str(prefix); + if let Some(entry) = bp.resolved(cx) { + classes.push('-'); + classes.push_str(entry.name); + } + if !suffix.is_empty() { + classes.push('-'); + classes.push_str(suffix); + } +} diff --git a/extensions/pagetop-bootsier/src/theme/bs/container.rs b/extensions/pagetop-bootsier/src/theme/bs/container.rs index 67526ab8..f846f47f 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/container.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/container.rs @@ -26,7 +26,7 @@ const EXTRA_WIDTH: &str = "bootsier.container.width"; /// /// let main = bs::Container::main() /// .with_id("main-page") -/// .with_width(bs::container::Width::From(BreakPoint::LG)) +/// .with_width(bs::container::Width::From(Breakpoint::Lg)) /// .with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Light))) /// .with_prop(PropsOp::add_classes(class::Text::with(BootsierColors::Dark))) /// .with_prop(PropsOp::add_classes(class::Border::with(ScaleSize::One))) @@ -61,7 +61,7 @@ pub enum Width { Default, /// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de /// ese punto de ruptura ocupa el 100% del ancho disponible. - From(BreakPoint), + From(Breakpoint), /// Ocupa el 100% del ancho disponible siempre. Fluid, /// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito. @@ -72,30 +72,34 @@ impl Width { const CONTAINER: &str = "container"; /// Añade la clase asociada al ancho del contenedor a la cadena de clases. + /// + /// El nombre del punto de ruptura se resuelve en el tema activo de `cx`. #[inline] - pub fn push_to(self, classes: &mut String) { + pub fn push_to(self, cx: &Context, classes: &mut String) { match self { - Self::Default => BreakPoint::None.push_to(classes, Self::CONTAINER, ""), - Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""), + Self::Default => { + push_breakpoint_class(cx, Breakpoint::Xs, classes, Self::CONTAINER, "") + } + Self::From(bp) => push_breakpoint_class(cx, bp, classes, Self::CONTAINER, ""), Self::Fluid | Self::FluidMax(_) => { - BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid") + push_breakpoint_class(cx, Breakpoint::Xs, classes, Self::CONTAINER, "fluid") } } } /// Devuelve la clase asociada al ancho del contenedor. - pub fn to_class(self) -> String { + pub fn to_class(self, cx: &Context) -> String { let mut class = String::new(); - self.push_to(&mut class); + self.push_to(cx, &mut class); class } } // **< Container SETUP >**************************************************************************** -pub(crate) fn setup(container: &mut Container) { +pub(crate) fn setup(container: &mut Container, cx: &Context) { let width = container.props().extra_or(EXTRA_WIDTH, Width::default()); - container.alter_prop(PropsOp::prepend_classes(width.to_class())); + container.alter_prop(PropsOp::prepend_classes(width.to_class(cx))); if let Width::FluidMax(w) = width && w.is_measurable() { diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs index c071f3fb..829f0f24 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown.rs @@ -168,7 +168,7 @@ pub(crate) async fn render( let auto_close = auto_close.opt_str(); let menu_classes = { let mut classes = "dropdown-menu".to_string(); - menu_align.push_to(&mut classes); + menu_align.push_to(cx, &mut classes); classes }; diff --git a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs index d6f76709..b1352bba 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/dropdown/props.rs @@ -112,65 +112,69 @@ impl Direction { /// Alineación horizontal del menú desplegable [`Dropdown`](crate::theme::bs::Dropdown). /// /// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una -/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]). +/// alineación diferente a partir de un punto de ruptura ([`Breakpoint`]). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum MenuAlign { /// Alineación al inicio (comportamiento por defecto). #[default] Start, /// Alineación al inicio a partir del punto de ruptura indicado. - StartAt(BreakPoint), + StartAt(Breakpoint), /// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido. - StartAndEnd(BreakPoint), + StartAndEnd(Breakpoint), /// Alineación al final. End, /// Alineación al final a partir del punto de ruptura indicado. - EndAt(BreakPoint), + EndAt(Breakpoint), /// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido. - EndAndStart(BreakPoint), + EndAndStart(Breakpoint), } impl MenuAlign { /// Añade las clases de alineación a la cadena de clases (sin incluir la base `dropdown-menu`). + /// + /// El nombre del punto de ruptura se resuelve en el tema activo de `cx`. #[inline] - pub fn push_to(self, classes: &mut String) { + pub fn push_to(self, cx: &Context, classes: &mut String) { + const MENU: &str = "dropdown-menu"; + match self { // Alineación por defecto (start), no añade clases extra. Self::Start => {} // `dropdown-menu-{bp}-start` Self::StartAt(bp) => { - bp.push_to(classes, "dropdown-menu", "start"); + push_breakpoint_class(cx, bp, classes, MENU, "start"); } // `dropdown-menu-start` + `dropdown-menu-{bp}-end` Self::StartAndEnd(bp) => { - BreakPoint::None.push_to(classes, "dropdown-menu", "start"); - bp.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "start"); + push_breakpoint_class(cx, bp, classes, MENU, "end"); } // `dropdown-menu-end` Self::End => { - BreakPoint::None.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "end"); } // `dropdown-menu-{bp}-end` Self::EndAt(bp) => { - bp.push_to(classes, "dropdown-menu", "end"); + push_breakpoint_class(cx, bp, classes, MENU, "end"); } // `dropdown-menu-end` + `dropdown-menu-{bp}-start` Self::EndAndStart(bp) => { - BreakPoint::None.push_to(classes, "dropdown-menu", "end"); - bp.push_to(classes, "dropdown-menu", "start"); + push_breakpoint_class(cx, Breakpoint::Xs, classes, MENU, "end"); + push_breakpoint_class(cx, bp, classes, MENU, "start"); } } } /// Devuelve las clases de alineación del menú (sin incluir la base `dropdown-menu`). - pub fn to_class(self) -> String { + pub fn to_class(self, cx: &Context) -> String { let mut classes = String::new(); - self.push_to(&mut classes); + self.push_to(cx, &mut classes); classes } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs index ab1d8a33..7700641a 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs @@ -42,7 +42,7 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let navbar = bs::Navbar::simple_toggle() -/// .with_expand(BreakPoint::MD) +/// .with_expand(Breakpoint::Md) /// .with_item(bs::navbar::Item::nav( /// bs::Nav::new() /// .with_item(bs::nav::Item::link(Lc::n("Home"), "/")) @@ -88,7 +88,7 @@ const EXTRA_EXPAND: &str = "bootsier.navbar.expand"; /// .with_route(Route::from("/")); /// /// let navbar = bs::Navbar::brand_right(brand) -/// .with_expand(BreakPoint::LG) +/// .with_expand(Breakpoint::Lg) /// .with_item(bs::navbar::Item::nav( /// bs::Nav::new() /// .with_kind(bs::nav::Kind::Pills) @@ -152,7 +152,7 @@ pub trait NavbarBootsier { 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. - fn with_expand(self, bp: BreakPoint) -> Self; + fn with_expand(self, bp: Breakpoint) -> Self; /// Define dónde se mostrará la barra de navegación dentro del documento. fn with_position(self, position: bs::navbar::Position) -> Self; @@ -187,7 +187,7 @@ impl NavbarBootsier for Navbar { navbar } - fn with_expand(mut self, bp: BreakPoint) -> Self { + fn with_expand(mut self, bp: Breakpoint) -> Self { self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp)); self } @@ -200,15 +200,15 @@ impl NavbarBootsier for Navbar { // **< Navbar SETUP >******************************************************************************* -pub(crate) fn setup(navbar: &mut Navbar) { +pub(crate) fn setup(navbar: &mut Navbar, cx: &Context) { // 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 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", ""); + push_breakpoint_class(cx, expand, &mut classes, "navbar-expand", ""); position.push_to(&mut classes); if !classes.is_empty() { navbar.alter_prop(PropsOp::add_classes(classes)); diff --git a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs index e9eed78e..8ee43ea8 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs @@ -48,7 +48,7 @@ pub struct Offcanvas { /// Devuelve el título del panel. title: Lc, /// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel. - breakpoint: BreakPoint, + breakpoint: Breakpoint, /// Devuelve el comportamiento configurado para la capa de fondo. backdrop: bs::offcanvas::Backdrop, /// Indica si la página principal puede desplazarse mientras el panel está abierto. @@ -77,8 +77,8 @@ impl Component for Offcanvas { // Clases CSS por defecto para el panel. self.alter_prop(PropsOp::prepend_classes({ - let mut classes = "offcanvas".to_string(); - self.breakpoint().push_to(&mut classes, "offcanvas", ""); + let mut classes = String::new(); + push_breakpoint_class(cx, *self.breakpoint(), &mut classes, "offcanvas", ""); self.placement().push_to(&mut classes); self.visibility().push_to(&mut classes); classes @@ -118,10 +118,10 @@ impl Offcanvas { /// ([`Offcanvas`]). /// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página. /// - /// Por ejemplo, con `BreakPoint::Lg`, será *offcanvas* en móviles y tabletas, y visible - /// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea + /// Por ejemplo, con `Breakpoint::Lg`, será *offcanvas* en móviles y tabletas, y visible + /// directamente en pantallas grandes. Por defecto usa `Breakpoint::Xs` para que sea /// *offcanvas* siempre. - pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self { + pub fn with_breakpoint(mut self, bp: Breakpoint) -> Self { self.breakpoint = bp; self } diff --git a/extensions/pagetop-bootsier/src/theme/token.rs b/extensions/pagetop-bootsier/src/theme/token.rs index edd0da0a..cf763304 100644 --- a/extensions/pagetop-bootsier/src/theme/token.rs +++ b/extensions/pagetop-bootsier/src/theme/token.rs @@ -1,8 +1,5 @@ //! Tipos enumerados para construir clases del tema. -mod breakpoint; -pub use breakpoint::BreakPoint; - mod color; pub use color::{BootsierColors, OpacityLevel}; diff --git a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs deleted file mode 100644 index 31310760..00000000 --- a/extensions/pagetop-bootsier/src/theme/token/breakpoint.rs +++ /dev/null @@ -1,61 +0,0 @@ -use pagetop::prelude::*; - -/// Puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum BreakPoint { - /// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical. - #[default] - None, - /// **576px o más**. Dispositivos pequeños: teléfonos en modo horizontal. - SM, - /// **768px o más**. Dispositivos medianos: tabletas. - MD, - /// **992px o más**. Dispositivos grandes: puestos de escritorio. - LG, - /// **1200px o más**. Dispositivos muy grandes: puestos de escritorio grandes. - XL, - /// **1400px o más**. Dispositivos extragrandes: puestos de escritorio más grandes. - XXL, -} - -impl BreakPoint { - /// Devuelve la identificación del punto de ruptura (`"sm"`, `"md"`, etc.), o `""` para `None`. - #[rustfmt::skip] - #[inline] - pub const fn as_str(self) -> &'static str { - match self { - Self::None => "", - Self::SM => "sm", - Self::MD => "md", - Self::LG => "lg", - Self::XL => "xl", - Self::XXL => "xxl", - } - } - - // Añade el punto de ruptura con un prefijo y un sufijo (opcional) a la cadena de clases. - // - // - Para `None`: `prefix` o `prefix-suffix` (si `suffix` no está vacío). - // - Para `SM..XXL`: `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`. - #[inline] - pub(crate) fn push_to(self, classes: &mut String, prefix: &str, suffix: &str) { - if prefix.is_empty() { - return; - } - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::None => classes.push_str(prefix), - _ => { - classes.push_str(prefix); - classes.push('-'); - classes.push_str(self.as_str()); - } - } - if !suffix.is_empty() { - classes.push('-'); - classes.push_str(suffix); - } - } -} diff --git a/extensions/pagetop-bootsier/tests/breakpoint_class.rs b/extensions/pagetop-bootsier/tests/breakpoint_class.rs new file mode 100644 index 00000000..fe6514d0 --- /dev/null +++ b/extensions/pagetop-bootsier/tests/breakpoint_class.rs @@ -0,0 +1,76 @@ +// Verifies that Bootsier builds its breakpoint-based Bootstrap classes from the breakpoint names +// of `Bootsier::breakpoint_entry()`, and that `Xxxl` (absent in Bootstrap) resolves as `xxl`. + +use pagetop::prelude::*; +use pagetop_bootsier::theme::*; + +fn cx() -> Context { + Context::default().with_theme(&pagetop_bootsier::Bootsier) +} + +#[pagetop::test] +async fn container_width_uses_breakpoint_name() { + let cx = cx(); + assert_eq!(bs::container::Width::Default.to_class(&cx), "container"); + assert_eq!(bs::container::Width::Fluid.to_class(&cx), "container-fluid"); + assert_eq!( + bs::container::Width::From(Breakpoint::Lg).to_class(&cx), + "container-lg" + ); +} + +#[pagetop::test] +async fn container_width_xxxl_resolves_as_xxl() { + let cx = cx(); + assert_eq!( + bs::container::Width::From(Breakpoint::Xxxl).to_class(&cx), + "container-xxl" + ); +} + +#[pagetop::test] +async fn dropdown_menu_align_combines_breakpoint_classes() { + let cx = cx(); + assert_eq!(bs::dropdown::MenuAlign::Start.to_class(&cx), ""); + assert_eq!( + bs::dropdown::MenuAlign::EndAt(Breakpoint::Md).to_class(&cx), + "dropdown-menu-md-end" + ); + assert_eq!( + bs::dropdown::MenuAlign::StartAndEnd(Breakpoint::Xl).to_class(&cx), + "dropdown-menu-start dropdown-menu-xl-end" + ); +} + +#[pagetop::test] +async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() { + let item = + || bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/"))); + + let mut navbar = bs::Navbar::simple().with_item(item()); + let html = navbar.render(&mut cx()).await.into_string(); + assert!(html.contains("navbar-expand-md")); + + let mut navbar = bs::Navbar::simple() + .with_expand(Breakpoint::Xxxl) + .with_item(item()); + let html = navbar.render(&mut cx()).await.into_string(); + assert!(html.contains("navbar-expand-xxl")); +} + +#[pagetop::test] +async fn offcanvas_uses_either_the_plain_or_the_breakpoint_class() { + // Bootstrap's `.offcanvas` is always fixed, so it must not accompany `.offcanvas-{bp}`. + let panel = + |offcanvas: bs::Offcanvas| offcanvas.with_child(Html::with(|_| html! { p { "Content" } })); + + let mut plain = panel(bs::Offcanvas::new()); + let html = plain.render(&mut cx()).await.into_string(); + assert!(html.contains(r#"class="offcanvas "#)); + assert!(!html.contains("offcanvas-lg")); + + let mut responsive = panel(bs::Offcanvas::new().with_breakpoint(Breakpoint::Lg)); + let html = responsive.render(&mut cx()).await.into_string(); + assert!(html.contains(r#"class="offcanvas-lg "#)); + assert!(!html.contains(r#"class="offcanvas "#)); +}