diff --git a/assets/css/basic.css b/assets/css/basic.css index b323e0e0..2fe4bb7c 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -268,20 +268,20 @@ body { margin-inline: auto; padding-inline: 1rem; } -@media (width >= 576px) { +@media (min-width: 576px) { .container, .container-sm { max-width: 540px; } } -@media (width >= 768px) { +@media (min-width: 768px) { .container, .container-sm, .container-md { max-width: 720px; } } -@media (width >= 992px) { +@media (min-width: 992px) { .container, .container-sm, .container-md, @@ -289,7 +289,7 @@ body { max-width: 960px; } } -@media (width >= 1200px) { +@media (min-width: 1200px) { .container, .container-sm, .container-md, @@ -298,7 +298,7 @@ body { max-width: 1140px; } } -@media (width >= 1400px) { +@media (min-width: 1400px) { .container, .container-sm, .container-md, @@ -308,7 +308,7 @@ body { max-width: 1320px; } } -@media (width >= 1920px) { +@media (min-width: 1920px) { .container, .container-sm, .container-md, @@ -687,13 +687,6 @@ input:disabled + label { color: var(--val-color--text--muted); pointer-events: none; } -/* The toggle of a dropdown inside a `Nav` looks like the other links: it drops the border that a - * standalone `Dropdown` toggle has (see "Dropdown component"). */ -.nav-link.dropdown-toggle { - width: 100%; - text-align: start; - border: 0; -} .nav-start { justify-content: flex-start; @@ -744,64 +737,17 @@ input:disabled + label { content: "\2630"; } -/* Pushes the toggle (`BrandLeft`) or the brand (`BrandRight`) to the far end of the bar. */ -.brand ~ .navbar-toggle, -.navbar-toggle ~ .brand { - margin-inline-start: auto; -} - .navbar-content { display: flex; align-items: center; gap: 1rem; flex: 1 1 auto; } -/* A bar without a toggle button never stacks its `.nav` items in a column, so they wrap onto - * another line when they don't fit instead of overflowing the bar. */ -.navbar-content .nav { - flex-wrap: wrap; -} .navbar-text { color: var(--val-color--text--muted); } -/* - * Navbar position, from the `navbar::Position` classes. `.navbar` has no background of its own, so - * a bar taken out of the flow (fixed) or pinned while scrolling (sticky) gets one, or the content - * passing underneath would show through. - */ -.navbar.fixed-top, -.navbar.fixed-bottom, -.navbar.sticky-top, -.navbar.sticky-bottom { - background-color: var(--val-color--bg); -} -.navbar.fixed-top, -.navbar.fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; -} -.navbar.fixed-top { - top: 0; -} -.navbar.fixed-bottom { - bottom: 0; -} -.navbar.sticky-top, -.navbar.sticky-bottom { - position: sticky; - z-index: 1020; -} -.navbar.sticky-top { - top: 0; -} -.navbar.sticky-bottom { - bottom: 0; -} - /* * A navbar with a toggle button collapses below the breakpoint set by its `navbar-expand-{name}` * class, added by the `Navbar` component. `navbar-expand` alone (a breakpoint with no minimum @@ -811,17 +757,16 @@ input:disabled + label { * * While collapsed, the `.nav` items are stacked in a column, so a dropdown menu inside it is laid * out in the flow (`position: static`) to push down the following items instead of covering them. - * A navbar without a toggle button has nothing to collapse: its `.nav` items stay in a row. */ -@media (width < 576px) { +@media (max-width: 575.98px) { .navbar-expand-sm { flex-wrap: wrap; } .navbar-expand-sm:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-sm:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -830,26 +775,26 @@ input:disabled + label { .navbar-expand-sm:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-sm:has(.navbar-toggle) .nav { + .navbar-expand-sm .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-sm:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-sm .nav .dropdown-menu { position: static; box-shadow: none; } } -@media (width < 768px) { +@media (max-width: 767.98px) { .navbar-expand-md { flex-wrap: wrap; } .navbar-expand-md:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-md:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -858,26 +803,26 @@ input:disabled + label { .navbar-expand-md:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-md:has(.navbar-toggle) .nav { + .navbar-expand-md .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-md:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-md .nav .dropdown-menu { position: static; box-shadow: none; } } -@media (width < 992px) { +@media (max-width: 991.98px) { .navbar-expand-lg { flex-wrap: wrap; } .navbar-expand-lg:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-lg:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -886,26 +831,26 @@ input:disabled + label { .navbar-expand-lg:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-lg:has(.navbar-toggle) .nav { + .navbar-expand-lg .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-lg:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-lg .nav .dropdown-menu { position: static; box-shadow: none; } } -@media (width < 1200px) { +@media (max-width: 1199.98px) { .navbar-expand-xl { flex-wrap: wrap; } .navbar-expand-xl:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-xl:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -914,26 +859,26 @@ input:disabled + label { .navbar-expand-xl:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-xl:has(.navbar-toggle) .nav { + .navbar-expand-xl .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-xl:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-xl .nav .dropdown-menu { position: static; box-shadow: none; } } -@media (width < 1400px) { +@media (max-width: 1399.98px) { .navbar-expand-xxl { flex-wrap: wrap; } .navbar-expand-xxl:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-xxl:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -942,26 +887,26 @@ input:disabled + label { .navbar-expand-xxl:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-xxl:has(.navbar-toggle) .nav { + .navbar-expand-xxl .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-xxl:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-xxl .nav .dropdown-menu { position: static; box-shadow: none; } } -@media (width < 1920px) { +@media (max-width: 1919.98px) { .navbar-expand-xxxl { flex-wrap: wrap; } .navbar-expand-xxxl:has(.navbar-toggle) .navbar-toggle { display: inline-block; + order: -1; } .navbar-expand-xxxl:has(.navbar-toggle) .navbar-content { - order: 1; display: none; flex-direction: column; align-items: stretch; @@ -970,12 +915,12 @@ input:disabled + label { .navbar-expand-xxxl:has(.navbar-toggle) .navbar-content.show { display: flex; } - .navbar-expand-xxxl:has(.navbar-toggle) .nav { + .navbar-expand-xxxl .nav { flex-direction: column; align-items: stretch; width: 100%; } - .navbar-expand-xxxl:has(.navbar-toggle) .nav .dropdown-menu { + .navbar-expand-xxxl .nav .dropdown-menu { position: static; box-shadow: none; } diff --git a/examples/locale/en-US/navbar-menus.ftl b/examples/locale/en-US/navbar-menus.ftl index 7ed6654c..98de2401 100644 --- a/examples/locale/en-US/navbar-menus.ftl +++ b/examples/locale/en-US/navbar-menus.ftl @@ -1,30 +1,7 @@ -menus_slogan = Navbars and menus -menus_note = Narrow the browser window to see how the navbars with a toggle button collapse. - -## Blocks - -menus_block_title_menus = Menus and dropdowns -menus_block_title_brand = Brand -menus_block_title_content = Other content -menus_block_title_colors = Color schemes -menus_block_title_container = Containers -menus_block_title_position = Placement -menus_block_title_layout = Layout and toggle button -menus_block_title_expand = Expand breakpoint -menus_block_title_offcanvas = Content in a side panel (Offcanvas) - -## Menus and dropdowns - -menus_title_full = Menu with dropdown, headers, dividers and a second menu at the end - -menus_item_home = Home menus_item_label = Label menus_item_link = Link menus_item_blank = External link menus_item_disabled = Disabled link -menus_item_action = Action -menus_item_another = Another action -menus_item_something = Something else menus_test_title = Dropdown @@ -44,66 +21,3 @@ menus_plugin_cache = Rust Plugin Cache menus_item_sign_up = Sign up menus_item_login = Login - -## Brand - -menus_title_brand_link = Text only, as a link -menus_title_brand_span = Text only, without a link -menus_title_brand_image = Image only -menus_title_brand_both = Image and text - -## Other content - -menus_title_form_search = Search form next to the brand -menus_title_form_buttons = Buttons -menus_title_form_group = Input group -menus_title_text = Text -menus_title_brand_and_text = Brand and text -menus_title_nav_text = Menu and text - -menus_search_label = Search -menus_search_button = Search -menus_button_main = Main button -menus_button_small = Smaller button -menus_group_placeholder = Username -menus_text_inline = Navbar text - -## Color schemes - -menus_title_color_dark = Dark background -menus_title_color_primary = Primary color with light text -menus_title_color_light = Light theme background -menus_title_color_custom = Custom color - -## Containers - -menus_title_container_full = Takes all the available width -menus_title_container_max = Inside a container with a maximum width - -## Placement - -menus_position_note = Navbars are anchored to the frame of each example, not to the window, so they don't cover the rest of the page. Scroll the frames with sticky navbars. -menus_title_position_fixed_top = Fixed top -menus_title_position_fixed_bottom = Fixed bottom -menus_title_position_sticky_top = Sticky top -menus_title_position_sticky_bottom = Sticky bottom -menus_filler = Page content. Fixed elements are taken out of the document flow, while sticky ones stay at the edge of the scrolling container. - -## Layout - -menus_title_layout_toggle = Toggle button only, no brand -menus_title_layout_brand_left = Brand on the left and toggle button on the right -menus_title_layout_brand_right = Toggle button on the left and brand on the right -menus_title_layout_brand_always = Always visible brand, no toggle button - -## Expand breakpoint - -menus_title_expand = Expands from { $name } ({ $width }) - -## Offcanvas - -menus_offcanvas_title = Main menu -menus_title_offcanvas_start = Panel from the start, no brand -menus_title_offcanvas_brand_left = Brand on the left and panel from the end -menus_title_offcanvas_brand_right = Brand on the right and panel without backdrop -menus_title_offcanvas_dark = Dark navbar and panel, with a static backdrop diff --git a/examples/locale/es-ES/navbar-menus.ftl b/examples/locale/es-ES/navbar-menus.ftl index 33617636..a87e407d 100644 --- a/examples/locale/es-ES/navbar-menus.ftl +++ b/examples/locale/es-ES/navbar-menus.ftl @@ -1,30 +1,7 @@ -menus_slogan = Barras de navegación y menús -menus_note = Reduce el ancho de la ventana para ver cómo colapsan las barras que tienen botón de despliegue. - -## Bloques - -menus_block_title_menus = Menús y desplegables -menus_block_title_brand = Marca de identidad -menus_block_title_content = Otros contenidos -menus_block_title_colors = Esquemas de color -menus_block_title_container = Contenedores -menus_block_title_position = Posición -menus_block_title_layout = Disposición y botón de despliegue -menus_block_title_expand = Punto de corte de expansión -menus_block_title_offcanvas = Contenido en un panel lateral (Offcanvas) - -## Menús y desplegables - -menus_title_full = Menú con desplegable, cabeceras, separadores y un segundo menú al final - -menus_item_home = Inicio menus_item_label = Etiqueta menus_item_link = Enlace menus_item_blank = Externo menus_item_disabled = Deshabilitado -menus_item_action = Acción -menus_item_another = Otra acción -menus_item_something = Algo más menus_test_title = Desplegable @@ -44,66 +21,3 @@ menus_plugin_cache = Plugin Rust de caché menus_item_sign_up = Registrarse menus_item_login = Iniciar sesión - -## Marca de identidad - -menus_title_brand_link = Sólo texto, como enlace -menus_title_brand_span = Sólo texto, sin enlace -menus_title_brand_image = Sólo imagen -menus_title_brand_both = Imagen y texto - -## Otros contenidos - -menus_title_form_search = Formulario de búsqueda junto a la marca -menus_title_form_buttons = Botones -menus_title_form_group = Grupo de campos -menus_title_text = Texto -menus_title_brand_and_text = Marca y texto -menus_title_nav_text = Menú y texto - -menus_search_label = Buscar -menus_search_button = Buscar -menus_button_main = Botón principal -menus_button_small = Botón pequeño -menus_group_placeholder = Usuario -menus_text_inline = Texto de la barra de navegación - -## Esquemas de color - -menus_title_color_dark = Fondo oscuro -menus_title_color_primary = Color primario con texto claro -menus_title_color_light = Fondo claro del tema -menus_title_color_custom = Color personalizado - -## Contenedores - -menus_title_container_full = Ocupa todo el ancho disponible -menus_title_container_max = Dentro de un contenedor de ancho máximo - -## Posición - -menus_position_note = Las barras se anclan al marco de cada ejemplo, no a la ventana, para no tapar el resto de la página. Desplaza el contenido de los marcos con barras pegajosas. -menus_title_position_fixed_top = Fija arriba -menus_title_position_fixed_bottom = Fija abajo -menus_title_position_sticky_top = Pegajosa arriba -menus_title_position_sticky_bottom = Pegajosa abajo -menus_filler = Contenido de la página. Los elementos con posición fija salen del flujo del documento, y los pegajosos se quedan en el borde del contenedor que se desplaza. - -## Disposición - -menus_title_layout_toggle = Sólo botón de despliegue, sin marca -menus_title_layout_brand_left = Marca a la izquierda y botón de despliegue a la derecha -menus_title_layout_brand_right = Botón de despliegue a la izquierda y marca a la derecha -menus_title_layout_brand_always = Marca siempre visible, sin botón de despliegue - -## Punto de corte - -menus_title_expand = Se expande a partir de { $name } ({ $width }) - -## Offcanvas - -menus_offcanvas_title = Menú principal -menus_title_offcanvas_start = Panel desde el inicio, sin marca -menus_title_offcanvas_brand_left = Marca a la izquierda y panel desde el final -menus_title_offcanvas_brand_right = Marca a la derecha y panel sin capa de fondo -menus_title_offcanvas_dark = Barra y panel oscuros, con capa de fondo estática diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 97c2009d..54cb37b7 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -1,783 +1,104 @@ use pagetop::prelude::*; -use pagetop_bootsier::theme::*; include_locales!(LOC from "examples/locale"); -struct NavbarMenus; +struct SuperMenu; #[async_trait] -impl Extension for NavbarMenus { +impl Extension for SuperMenu { fn dependencies(&self) -> Vec { - vec![&pagetop_bootsier::Bootsier] + vec![ + &pagetop_aliner::Aliner, + &pagetop_bootsier::Bootsier, + &pagetop::base::extension::Welcome, + ] } - fn configure_router(&self, router: Router) -> Router { - router.route("/", web::get(navbar_menus)) - } -} - -async fn navbar_menus(request: HttpRequest) -> Result { - let page = Page::new(request) - .with_assets(demo_title_styles()) - .with_assets(demo_code_styles()) - .with_assets(demo_frame_styles()) - .with_assets(demo_scroll_styles()); - - let mut intro = Intro::custom() - .with_width(intro::Width::Full) - .with_title(Lc::n("PageTop")) - .with_slogan(Lc::t("menus_slogan", &LOC)) - .with_child(Html::with(|cx| { - html! { - p class="intro-text-lead" { (Lc::t("menus_note", &LOC).using(cx)) } - } - })) - .with_child(menus_block()) - .with_child(brand_block()) - .with_child(container_block()) - .with_child(layout_block()) - .with_child(expand_block()) - .with_child(content_block()) - .with_child(position_block()); - - // Demostraciones asociadas a Bootsier: usan sus tipos, o clases y variables CSS de Bootstrap - // que otros temas no definen. Sólo se muestran con el tema Bootsier activo. - if is_bootsier() { - intro = intro - .with_child(colors_block()) - .with_child(offcanvas_block()); - } - - page.with_child(intro).render().await -} - -// **< Menús >************************************************************************************** - -fn menus_block() -> Block { - let full_menu = Navbar::brand_left(demo_brand()) - .with_prop(light_bg()) - .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav( - Nav::new() - .with_item(nav::Item::link(Lc::t("menus_item_home", &LOC), "/").with_active(true)) - .with_item(nav::Item::link_blank( - Lc::t("menus_item_blank", &LOC), - "https://docs.rs/pagetop", - )) - .with_item(nav::Item::dropdown( - Dropdown::new() - .with_title(Lc::t("menus_test_title", &LOC)) - .with_item(dropdown::Item::header(Lc::t("menus_dev_header", &LOC))) - .with_item(dropdown::Item::link( - Lc::t("menus_dev_getting_started", &LOC), - "/dev/getting-started", - )) - .with_item(dropdown::Item::link( - Lc::t("menus_dev_guides", &LOC), - "/dev/guides", - )) - .with_item(dropdown::Item::link_blank( - Lc::t("menus_dev_forum", &LOC), - "https://forum.example.dev", - )) - .with_item(dropdown::Item::divider()) - .with_item(dropdown::Item::header(Lc::t("menus_sdk_header", &LOC))) - .with_item(dropdown::Item::link( - Lc::t("menus_sdk_rust", &LOC), - "/dev/sdks/rust", - )) - .with_item(dropdown::Item::link( - Lc::t("menus_sdk_js", &LOC), - "/dev/sdks/js", - )) - .with_item(dropdown::Item::link( - Lc::t("menus_sdk_python", &LOC), - "/dev/sdks/python", - )) - .with_item(dropdown::Item::divider()) - .with_item(dropdown::Item::header(Lc::t("menus_plugin_header", &LOC))) - .with_item(dropdown::Item::link( - Lc::t("menus_plugin_auth", &LOC), - "/dev/sdks/rust/plugins/auth", - )) - .with_item(dropdown::Item::link( - Lc::t("menus_plugin_cache", &LOC), - "/dev/sdks/rust/plugins/cache", - )) - .with_item(dropdown::Item::divider()) - .with_item(dropdown::Item::label(Lc::t("menus_item_label", &LOC))) - .with_item(dropdown::Item::link_disabled( - Lc::t("menus_item_disabled", &LOC), - "#", - )), - )) - .with_item(nav::Item::link_disabled( - Lc::t("menus_item_disabled", &LOC), - "#", - )), - )) - .with_item(navbar::Item::nav( - Nav::new() - // Empuja este menú (y lo que le siga) al extremo final de la barra. - .with_prop(FlexItem::push_end()) - .with_item(nav::Item::link( - Lc::t("menus_item_sign_up", &LOC), - "/auth/sign-up", - )) - .with_item(nav::Item::link( - Lc::t("menus_item_login", &LOC), - "/auth/login", - )), - )); - - add_demo( - Block::new().with_title(Lc::t("menus_block_title_menus", &LOC)), - Lc::t("menus_title_full", &LOC), - concat!( - "Navbar::brand_left(brand)", - ".with_item(navbar::Item::nav(Nav::new()...with_item(nav::Item::dropdown(...))))", - ".with_item(navbar::Item::nav(Nav::new().with_prop(FlexItem::push_end())...))", - ), - full_menu, - ) -} - -// **< Marca de identidad >************************************************************************* - -fn brand_block() -> Block { - // Una barra sin contenidos no se renderiza, así que la marca se añade como un elemento más. - let brand_variants: [(&str, Brand, &str); 4] = [ - ( - "menus_title_brand_link", - demo_brand(), - "navbar::Item::brand(Brand::new().with_title(...).with_route(Route::from(\"/\")))", - ), - ( - "menus_title_brand_span", - demo_brand().with_route(None::), - "navbar::Item::brand(Brand::new().with_title(...).with_route(None))", - ), - ( - "menus_title_brand_image", - Brand::new() - .with_title(Lc::none()) - .with_image(demo_logo()) - .with_route(Route::from("/")), - "navbar::Item::brand(Brand::new().with_title(Lc::none()).with_image(image))", - ), - ( - "menus_title_brand_both", - demo_brand().with_image(demo_logo()), - "navbar::Item::brand(Brand::new().with_title(...).with_image(image))", - ), - ]; - - let mut block = Block::new().with_title(Lc::t("menus_block_title_brand", &LOC)); - for (title_key, brand, code) in brand_variants { - block = add_demo( - block, - Lc::t(title_key, &LOC), - code, - Navbar::simple() - .with_prop(light_bg()) - .with_item(navbar::Item::brand(brand)), - ); - } - block -} - -// **< Otros contenidos >*************************************************************************** - -fn content_block() -> Block { - let gap = align::Gap::Both(UnitValue::RelRem(0.5)); - - let search_form = Form::new() - .with_action("/") - .with_method(form::Method::Get) - .with_child( - Flex::new() - .with_align(align::Items::Center) - .with_gap(gap) - .with_child( - form::input::Field::search() - .with_name("q") - .with_placeholder(Lc::t("menus_search_label", &LOC)) - // Sin el margen inferior con que algunos temas separan los campos. - .with_prop(Margin::new().with_bottom(UnitValue::Zero)), - ) - .with_child( - Button::submit(Lc::t("menus_search_button", &LOC)) - .with_style(button::Style::Outline(Intent::Success)), - ), - ); - - let buttons = Form::new().with_child( - Flex::new() - .with_align(align::Items::Center) - .with_gap(gap) - .with_child( - Button::plain(Lc::t("menus_button_main", &LOC)) - .with_style(button::Style::Outline(Intent::Success)), - ) - .with_child( - Button::plain(Lc::t("menus_button_small", &LOC)) - .with_size(button::Size::Small) - .with_style(button::Style::Outline(Intent::Neutral)), - ), - ); - - let mut content_variants: Vec<(&str, Navbar, &str)> = vec![ - ( - "menus_title_form_search", - Navbar::simple_brand_left(demo_brand()).with_item(navbar::Item::form(search_form)), - "Navbar::simple_brand_left(brand).with_item(navbar::Item::form(Form::new()...))", - ), - ( - "menus_title_form_buttons", - Navbar::simple().with_item(navbar::Item::form(buttons)), - "Navbar::simple().with_item(navbar::Item::form(Form::new()...))", - ), - ( - "menus_title_text", - Navbar::simple().with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))), - "Navbar::simple().with_item(navbar::Item::text(Lc::t(...)))", - ), - ( - "menus_title_brand_and_text", - Navbar::simple() - .with_item(navbar::Item::brand(demo_brand())) - .with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))), - concat!( - "Navbar::simple().with_item(navbar::Item::brand(brand))", - ".with_item(navbar::Item::text(...))", - ), - ), - ( - "menus_title_nav_text", - Navbar::brand_left(demo_brand()) - .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav( - demo_nav().with_prop(Margin::new().with_end(UnitValue::Auto)), - )) - .with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))), - concat!( - "Navbar::brand_left(brand).with_item(navbar::Item::nav(", - "Nav::new().with_prop(Margin::new().with_end(UnitValue::Auto))...))", - ".with_item(navbar::Item::text(...))", - ), - ), - ]; - - // El núcleo no tiene un componente de grupo de entrada, así que esta demostración usa clases de - // Bootstrap y sólo se muestra con el tema Bootsier. - if is_bootsier() { - let input_group = Nav::new().with_item(nav::Item::html(Html::with(|cx| { - html! { - form { - div class="input-group" { - span class="input-group-text" { "@" } - input - class="form-control" - type="text" - placeholder=[Lc::t("menus_group_placeholder", &LOC).lookup(cx)] - aria-label=[Lc::t("menus_group_placeholder", &LOC).lookup(cx)]; - } - } - } - }))); - content_variants.insert( - 2, - ( - "menus_title_form_group", - Navbar::simple().with_item(navbar::Item::nav(input_group)), - "Navbar::simple().with_item(navbar::Item::nav(Nav::new()...nav::Item::html(...)))", - ), - ); - } - - let mut block = Block::new().with_title(Lc::t("menus_block_title_content", &LOC)); - for (title_key, navbar, code) in content_variants { - block = add_demo( - block, - Lc::t(title_key, &LOC), - code, - navbar.with_prop(light_bg()), - ); - } - block -} - -// **< Esquemas de color >************************************************************************** - -fn colors_block() -> Block { - let color_variants: [(&str, Vec, &str); 4] = [ - ( - "menus_title_color_dark", - vec![ - PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)), - PropsOp::set("data-bs-theme", "dark"), - ], - concat!( - "Navbar::brand_left(brand)", - ".with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)))", - ".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))", - ), - ), - ( - "menus_title_color_primary", - vec![ - PropsOp::add_classes(class::Bg::with(BootsierColors::Primary)), - PropsOp::set("data-bs-theme", "dark"), - ], - concat!( - "Navbar::brand_left(brand)", - ".with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Primary)))", - ".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))", - ), - ), - ( - "menus_title_color_light", - vec![light_bg()], - concat!( - "Navbar::brand_left(brand)", - ".with_prop(PropsOp::add_classes(class::Bg::with(class::BgColor::BodyTertiary)))", - ), - ), - ( - "menus_title_color_custom", - vec![PropsOp::add_style("background-color", "#e3f2fd")], - concat!( - "Navbar::brand_left(brand)", - ".with_prop(PropsOp::add_style(\"background-color\", \"#e3f2fd\"))", - ), - ), - ]; - - let mut block = Block::new().with_title(Lc::t("menus_block_title_colors", &LOC)); - for (title_key, props, code) in color_variants { - let mut navbar = Navbar::brand_left(demo_brand()) + async fn initialize(&self) { + let navbar_menu = Navbar::brand_left(Brand::new()) .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav(demo_nav())); - for op in props { - navbar = navbar.with_prop(op); - } - block = add_demo(block, Lc::t(title_key, &LOC), code, navbar); - } - block -} + .with_item(navbar::Item::nav( + Nav::new() + .with_item(nav::Item::link(Lc::t("menus_item_link", &LOC), "/")) + .with_item(nav::Item::link_blank( + Lc::t("menus_item_blank", &LOC), + "https://docs.rs/pagetop", + )) + .with_item(nav::Item::dropdown( + Dropdown::new() + .with_title(Lc::t("menus_test_title", &LOC)) + .with_item(dropdown::Item::header(Lc::t("menus_dev_header", &LOC))) + .with_item(dropdown::Item::link( + Lc::t("menus_dev_getting_started", &LOC), + "/dev/getting-started", + )) + .with_item(dropdown::Item::link( + Lc::t("menus_dev_guides", &LOC), + "/dev/guides", + )) + .with_item(dropdown::Item::link_blank( + Lc::t("menus_dev_forum", &LOC), + "https://forum.example.dev", + )) + .with_item(dropdown::Item::divider()) + .with_item(dropdown::Item::header(Lc::t("menus_sdk_header", &LOC))) + .with_item(dropdown::Item::link( + Lc::t("menus_sdk_rust", &LOC), + "/dev/sdks/rust", + )) + .with_item(dropdown::Item::link( + Lc::t("menus_sdk_js", &LOC), + "/dev/sdks/js", + )) + .with_item(dropdown::Item::link( + Lc::t("menus_sdk_python", &LOC), + "/dev/sdks/python", + )) + .with_item(dropdown::Item::divider()) + .with_item(dropdown::Item::header(Lc::t("menus_plugin_header", &LOC))) + .with_item(dropdown::Item::link( + Lc::t("menus_plugin_auth", &LOC), + "/dev/sdks/rust/plugins/auth", + )) + .with_item(dropdown::Item::link( + Lc::t("menus_plugin_cache", &LOC), + "/dev/sdks/rust/plugins/cache", + )) + .with_item(dropdown::Item::divider()) + .with_item(dropdown::Item::label(Lc::t("menus_item_label", &LOC))) + .with_item(dropdown::Item::link_disabled( + Lc::t("menus_item_disabled", &LOC), + "#", + )), + )) + .with_item(nav::Item::link_disabled( + Lc::t("menus_item_disabled", &LOC), + "#", + )), + )) + .with_item(navbar::Item::nav( + Nav::new() + // Empuja este menú (y lo que le siga) al extremo final de la barra. + .with_prop(FlexItem::push_end()) + .with_item(nav::Item::link( + Lc::t("menus_item_sign_up", &LOC), + "/auth/sign-up", + )) + .with_item(nav::Item::link( + Lc::t("menus_item_login", &LOC), + "/auth/login", + )), + )); -// **< Contenedores >******************************************************************************* - -fn container_block() -> Block { - let navbar = || { - Navbar::brand_left(demo_brand()) - .with_prop(light_bg()) - .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav(demo_nav())) - }; - - let block = Block::new().with_title(Lc::t("menus_block_title_container", &LOC)); - let block = add_demo( - block, - Lc::t("menus_title_container_full", &LOC), - "Navbar::brand_left(brand)...", - navbar(), - ); - add_demo( - block, - Lc::t("menus_title_container_max", &LOC), - concat!( - "Container::new()", - ".with_width(container::Width::FluidMax(UnitValue::RelRem(40.0)))", - ".with_child(Navbar::brand_left(brand)...)", - ), - Container::new() - .with_width(container::Width::FluidMax(UnitValue::RelRem(40.0))) - .with_child(navbar()), - ) -} - -// **< Posición >*********************************************************************************** - -fn position_block() -> Block { - let navbar = |position| { - Navbar::simple() - .with_position(position) - .with_prop(light_bg()) - .with_item(navbar::Item::nav(demo_nav())) - }; - - // Las barras fijas se anclan al marco y no a la ventana, para no tapar el resto de la página. - let fixed_variants: [(&str, navbar::Position, &str); 2] = [ - ( - "menus_title_position_fixed_top", - navbar::Position::FixedTop, - "Navbar::simple().with_position(navbar::Position::FixedTop)", - ), - ( - "menus_title_position_fixed_bottom", - navbar::Position::FixedBottom, - "Navbar::simple().with_position(navbar::Position::FixedBottom)", - ), - ]; - - let mut block = Block::new() - .with_title(Lc::t("menus_block_title_position", &LOC)) - .with_child(Html::with(|cx| { - html! { - p { (Lc::t("menus_position_note", &LOC).using(cx)) } - } - })); - for (title_key, position, code) in fixed_variants { - block = add_demo( - block, - Lc::t(title_key, &LOC), - code, + InRegion::Global(&CoreRegions::Header).add( Container::new() - .with_width(container::Width::Fluid) - .with_prop(PropsOp::add_classes("menus-demo-frame")) - .with_child(navbar(position)) - .with_child(filler()), + .with_width(container::Width::FluidMax(UnitValue::RelRem(75.0))) + .with_child(navbar_menu), ); } - - // Con `sticky` la barra ocupa su lugar en el flujo y se queda pegada al borde del contenedor - // que se desplaza (aquí un marco con scroll). - block = add_demo( - block, - Lc::t("menus_title_position_sticky_top", &LOC), - "Navbar::simple().with_position(navbar::Position::StickyTop)", - Container::new() - .with_width(container::Width::Fluid) - .with_prop(PropsOp::add_classes("menus-demo-scroll")) - .with_child(navbar(navbar::Position::StickyTop)) - .with_child(filler()) - .with_child(filler()) - .with_child(filler()) - .with_child(filler()), - ); - add_demo( - block, - Lc::t("menus_title_position_sticky_bottom", &LOC), - "Navbar::simple().with_position(navbar::Position::StickyBottom)", - Container::new() - .with_width(container::Width::Fluid) - .with_prop(PropsOp::add_classes("menus-demo-scroll")) - .with_child(filler()) - .with_child(filler()) - .with_child(filler()) - .with_child(filler()) - .with_child(navbar(navbar::Position::StickyBottom)), - ) -} - -// **< Disposición >******************************************************************************** - -fn layout_block() -> Block { - let layout_variants: [(&str, Navbar, &str); 4] = [ - ( - "menus_title_layout_toggle", - Navbar::simple_toggle(), - "Navbar::simple_toggle()", - ), - ( - "menus_title_layout_brand_left", - Navbar::brand_left(demo_brand()), - "Navbar::brand_left(brand)", - ), - ( - "menus_title_layout_brand_right", - Navbar::brand_right(demo_brand()), - "Navbar::brand_right(brand)", - ), - ( - "menus_title_layout_brand_always", - Navbar::simple_brand_left(demo_brand()), - "Navbar::simple_brand_left(brand)", - ), - ]; - - let mut block = Block::new().with_title(Lc::t("menus_block_title_layout", &LOC)); - for (title_key, navbar, code) in layout_variants { - block = add_demo( - block, - Lc::t(title_key, &LOC), - code, - navbar - .with_prop(light_bg()) - .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav(demo_nav())), - ); - } - block -} - -// **< Punto de corte >***************************************************************************** - -fn expand_block() -> Block { - let expand_variants: [(Lc, Breakpoint, &str); 5] = [ - ( - expand_title("sm", "576px"), - Breakpoint::Sm, - "Navbar::brand_left(brand).with_expand(Breakpoint::Sm)", - ), - ( - expand_title("md", "768px"), - Breakpoint::Md, - "Navbar::brand_left(brand).with_expand(Breakpoint::Md)", - ), - ( - expand_title("lg", "992px"), - Breakpoint::Lg, - "Navbar::brand_left(brand).with_expand(Breakpoint::Lg)", - ), - ( - expand_title("xl", "1200px"), - Breakpoint::Xl, - "Navbar::brand_left(brand).with_expand(Breakpoint::Xl)", - ), - ( - expand_title("xxl", "1400px"), - Breakpoint::Xxl, - "Navbar::brand_left(brand).with_expand(Breakpoint::Xxl)", - ), - ]; - - let mut block = Block::new().with_title(Lc::t("menus_block_title_expand", &LOC)); - for (title, breakpoint, code) in expand_variants { - block = add_demo( - block, - title, - code, - Navbar::brand_left(demo_brand()) - .with_prop(light_bg()) - .with_expand(breakpoint) - .with_item(navbar::Item::nav(demo_nav())), - ); - } - block -} - -fn expand_title(name: &'static str, width: &'static str) -> Lc { - Lc::t("menus_title_expand", &LOC) - .with_arg("name", name) - .with_arg("width", width) -} - -// **< Offcanvas >********************************************************************************** - -fn offcanvas_block() -> Block { - let panel = |placement, backdrop| { - bs::navbar::Panel::new() - .with_title(Lc::t("menus_offcanvas_title", &LOC)) - .with_placement(placement) - .with_backdrop(backdrop) - }; - - let offcanvas_variants: [(&str, Navbar, &str); 4] = [ - ( - "menus_title_offcanvas_start", - Navbar::offcanvas(panel( - bs::offcanvas::Placement::Start, - bs::offcanvas::Backdrop::Enabled, - )), - concat!( - "Navbar::offcanvas(bs::navbar::Panel::new()", - ".with_placement(bs::offcanvas::Placement::Start))", - ), - ), - ( - "menus_title_offcanvas_brand_left", - Navbar::offcanvas_brand_left( - demo_brand(), - panel( - bs::offcanvas::Placement::End, - bs::offcanvas::Backdrop::Enabled, - ), - ), - concat!( - "Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()", - ".with_placement(bs::offcanvas::Placement::End))", - ), - ), - ( - "menus_title_offcanvas_brand_right", - Navbar::offcanvas_brand_right( - demo_brand(), - panel( - bs::offcanvas::Placement::Start, - bs::offcanvas::Backdrop::Disabled, - ), - ), - concat!( - "Navbar::offcanvas_brand_right(brand, bs::navbar::Panel::new()", - ".with_backdrop(bs::offcanvas::Backdrop::Disabled))", - ), - ), - ( - "menus_title_offcanvas_dark", - Navbar::offcanvas_brand_left( - demo_brand(), - panel( - bs::offcanvas::Placement::End, - bs::offcanvas::Backdrop::Static, - ), - ) - .with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark))) - .with_prop(PropsOp::set("data-bs-theme", "dark")), - concat!( - "Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()", - ".with_backdrop(bs::offcanvas::Backdrop::Static))", - ".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))", - ), - ), - ]; - - let mut block = Block::new().with_title(Lc::t("menus_block_title_offcanvas", &LOC)); - for (title_key, navbar, code) in offcanvas_variants { - let navbar = navbar - .with_expand(Breakpoint::Lg) - .with_item(navbar::Item::nav(demo_nav())); - // La barra oscura ya define su propio fondo. - let navbar = if title_key == "menus_title_offcanvas_dark" { - navbar - } else { - navbar.with_prop(light_bg()) - }; - block = add_demo(block, Lc::t(title_key, &LOC), code, navbar); - } - block -} - -// **< HELPERS >************************************************************************************ - -// Marca de identidad de las demostraciones. -fn demo_brand() -> Brand { - Brand::new() - .with_title(Lc::n("PageTop")) - .with_route(Route::from("/")) -} - -// Logotipo de las demostraciones, con el tamaño habitual de una marca en una barra. -fn demo_logo() -> Image { - Image::with(image::Source::logo(PageTopSvg::Tile)) - .with_size(image::Size::Both(UnitValue::Px(36))) -} - -// Menú común a la mayoría de las demostraciones: activo, enlace, desplegable y deshabilitado. -fn demo_nav() -> Nav { - Nav::new() - .with_item(nav::Item::link(Lc::t("menus_item_home", &LOC), "/").with_active(true)) - .with_item(nav::Item::link(Lc::t("menus_item_link", &LOC), "/link")) - .with_item(nav::Item::dropdown( - Dropdown::new() - .with_title(Lc::t("menus_test_title", &LOC)) - .with_item(dropdown::Item::link( - Lc::t("menus_item_action", &LOC), - "/action", - )) - .with_item(dropdown::Item::link( - Lc::t("menus_item_another", &LOC), - "/another", - )) - .with_item(dropdown::Item::divider()) - .with_item(dropdown::Item::link( - Lc::t("menus_item_something", &LOC), - "/something", - )), - )) - .with_item(nav::Item::link_disabled( - Lc::t("menus_item_disabled", &LOC), - "#", - )) -} - -fn is_bootsier() -> bool { - global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") -} - -// Fondo claro para que las barras destaquen sobre el fondo de la página. Basic no da fondo a -// `.navbar`, así que con otros temas se usa el color de fondo de Basic (o un gris claro si el tema -// activo tampoco lo define). -fn light_bg() -> PropsOp { - if is_bootsier() { - PropsOp::add_classes(class::Bg::with(class::BgColor::BodyTertiary)) - } else { - PropsOp::add_style("background-color", "var(--val-color--bg, #f8f9fa)") - } -} - -// Título y fragmento de código que introducen cada demostración, seguidos de ésta. -fn add_demo(block: Block, title: Lc, code: &'static str, demo: impl Into) -> Block { - block - .with_child(caption(title, Lc::n(code))) - .with_child(demo) -} - -// Párrafo de relleno para que los marcos de demostración tengan contenido que desplazar. -fn filler() -> Html { - Html::with(|cx| { - html! { - p { (Lc::t("menus_filler", &LOC).using(cx)) } - } - }) -} - -// Título y fragmento de código que introducen cada demostración. -fn caption(title: Lc, code: Lc) -> Html { - Html::with(move |cx| { - html! { - h3 class="menus-demo-title" { (title.using(cx)) } - p { code class="menus-demo-code" { (code.using(cx)) } } - } - }) -} - -// Separa el título de cada demostración de la barra (o marco) que le precede. -fn demo_title_styles() -> AssetsOp { - AssetsOp::add_responsive_styles(None, "menus-demo-title", [("margin-top", "1.5rem")]) -} - -// Evita que los fragmentos de código largos desborden su contenedor. -fn demo_code_styles() -> AssetsOp { - AssetsOp::add_responsive_styles(None, "menus-demo-code", [("overflow-wrap", "anywhere")]) -} - -// Marco de tamaño fijo para las barras con posición `fixed`. -fn demo_frame_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "menus-demo-frame", - [ - ("position", "relative"), - // Un `transform` convierte al marco en el bloque contenedor de sus descendientes con - // `position: fixed`, así la barra se ancla al marco en vez de a la ventana. - ("transform", "translateZ(0)"), - ("height", "12rem"), - ("padding", "4rem 1rem"), - ("overflow", "hidden"), - ("border", "1px solid #dee2e6"), - ("margin", "0 0 1.5rem"), - ], - ) -} - -// Marco de tamaño fijo con desplazamiento, para las barras con posición `sticky`. -fn demo_scroll_styles() -> AssetsOp { - AssetsOp::add_responsive_styles( - None, - "menus-demo-scroll", - [ - ("height", "12rem"), - ("overflow-y", "auto"), - ("padding", "0 1rem"), - ("border", "1px solid #dee2e6"), - ("margin", "0 0 1.5rem"), - ], - ) } #[pagetop::main] async fn main() -> std::io::Result<()> { - Application::prepare(&NavbarMenus).await.run().await + Application::prepare(&SuperMenu).await.run().await } diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar.rs index aae33cd5..e79213dd 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar.rs @@ -8,15 +8,12 @@ //! que identifique la compañía, producto o nombre del proyecto asociado a la solución web. mod props; -pub use props::Layout; +pub use props::{Layout, Position}; pub use super::Brand; pub use pagetop::base::component::Navbar; -pub use pagetop::base::component::navbar::{Item, Position}; - -mod panel; -pub use panel::Panel; +pub use pagetop::base::component::navbar::Item; mod component; pub use component::NavbarBootsier; diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs index 07d53923..48059ec7 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs @@ -7,17 +7,18 @@ const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_OFFCANVAS: &str = "offcanvas"; const EXTRA_LAYOUT: &str = "bootsier.navbar.layout"; +const EXTRA_POSITION: &str = "bootsier.navbar.position"; /// Extensión de Bootsier para [`Navbar`]. /// /// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con -/// botón de despliegue o dentro de un [`navbar::Panel`]), controladas por [`navbar::Layout`]. -/// También puede fijarse en la parte superior o inferior del documento con [`navbar::Position`]. El +/// 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`]. El /// punto de corte a partir del cual deja de colapsar se define con [`Navbar::with_expand()`]. /// /// [`Navbar`]: crate::theme::bs::Navbar +/// [`Offcanvas`]: crate::theme::bs::Offcanvas /// [`navbar::Layout`]: crate::theme::bs::navbar::Layout -/// [`navbar::Panel`]: crate::theme::bs::navbar::Panel /// [`navbar::Position`]: crate::theme::bs::navbar::Position /// /// # Ejemplos @@ -102,12 +103,13 @@ const EXTRA_LAYOUT: &str = "bootsier.navbar.layout"; /// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; -/// let panel = bs::navbar::Panel::new() +/// let oc = bs::Offcanvas::new() +/// .with_id("main_offcanvas") /// .with_title(Lc::n("Main menu")) /// .with_placement(bs::offcanvas::Placement::Start) /// .with_backdrop(bs::offcanvas::Backdrop::Enabled); /// -/// let navbar = bs::Navbar::offcanvas(panel) +/// let navbar = bs::Navbar::offcanvas(oc) /// .with_item(bs::navbar::Item::nav( /// bs::Nav::new() /// .with_item(bs::nav::Item::link(Lc::n("Home"), "/")) @@ -141,73 +143,64 @@ const EXTRA_LAYOUT: &str = "bootsier.navbar.layout"; /// ``` #[builder_impl] pub trait NavbarBootsier { - /// Crea una barra de navegación cuyo contenido se muestra en un panel lateral. - fn offcanvas(panel: bs::navbar::Panel) -> Self; + /// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**. + fn offcanvas(oc: bs::Offcanvas) -> Self; - /// Crea una barra de navegación con **marca de identidad a la izquierda** y contenido en un - /// panel lateral. - fn offcanvas_brand_left(brand: Brand, panel: bs::navbar::Panel) -> Self; + /// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**. + fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self; - /// Crea una barra de navegación con **marca de identidad a la derecha** y contenido en un panel - /// lateral. - fn offcanvas_brand_right(brand: Brand, panel: bs::navbar::Panel) -> Self; + /// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**. + fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self; + + /// Define dónde se mostrará la barra de navegación dentro del documento. + fn with_position(self, position: bs::navbar::Position) -> Self; } #[builder_impl] impl NavbarBootsier for Navbar { - fn offcanvas(panel: bs::navbar::Panel) -> Self { + fn offcanvas(oc: bs::Offcanvas) -> Self { let mut navbar = Self::new(); navbar.alter_prop(PropsOp::set_extra( EXTRA_LAYOUT, - bs::navbar::Layout::Offcanvas(panel), + bs::navbar::Layout::Offcanvas(Embed::with(oc)), )); navbar } - fn offcanvas_brand_left(brand: Brand, panel: bs::navbar::Panel) -> Self { + fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self { let mut navbar = Self::new(); navbar.alter_prop(PropsOp::set_extra( EXTRA_LAYOUT, - bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), panel), + bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), Embed::with(oc)), )); navbar } - fn offcanvas_brand_right(brand: Brand, panel: bs::navbar::Panel) -> Self { + fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self { let mut navbar = Self::new(); navbar.alter_prop(PropsOp::set_extra( EXTRA_LAYOUT, - bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), panel), + bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), Embed::with(oc)), )); navbar } + + fn with_position(mut self, position: bs::navbar::Position) -> Self { + self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position)); + self + } } // **< Navbar SETUP >******************************************************************************* pub(crate) fn setup(navbar: &mut Navbar) { - // Sin botón de despliegue no hay nada que colapsar, así que el punto de corte no debe afectar a - // la barra: Bootstrap apilaría igualmente el menú por debajo de él. `navbar-expand` a secas es - // su «expandida siempre». Las disposiciones de Bootsier (`EXTRA_LAYOUT`, con panel lateral) - // siempre llevan botón; de las de base sólo `Simple` y `SimpleBrandLeft` no lo llevan. - let has_toggle = navbar + let position = navbar .props() - .extra::(EXTRA_LAYOUT) - .is_ok() - || !matches!( - navbar.layout(), - navbar::Layout::Simple | navbar::Layout::SimpleBrandLeft(_) - ); - if !has_toggle { - let expand = navbar.props().get_classes().and_then(|classes| { - classes - .split_whitespace() - .find(|class| class.starts_with("navbar-expand-")) - .map(String::from) - }); - if let Some(class) = expand { - navbar.alter_prop(PropsOp::replace_classes(class, "navbar-expand")); - } + .extra_or(EXTRA_POSITION, bs::navbar::Position::default()); + let mut classes = String::new(); + position.push_to(&mut classes); + if !classes.is_empty() { + navbar.alter_prop(PropsOp::add_classes(classes)); } } @@ -243,16 +236,13 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result(EXTRA_LAYOUT) @@ -270,23 +260,24 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result { + @let id_content = util::join!(&id, "-content"); + (button(cx, TOGGLE_COLLAPSE, &id_content)) div id=(&id_content) class="collapse navbar-collapse" { (items) } }, - // Barra con marca a la izquierda, siempre visible. El contenedor deja los - // contenidos junto a la marca: sin él, el `space-between` de `container-fluid` - // los llevaría al extremo opuesto. Sin `collapse`, `setup()` garantiza - // `navbar-expand` y Bootstrap lo muestra siempre. + // Barra con marca a la izquierda, siempre visible. bs::navbar::Layout::SimpleBrandLeft(brand) => { (brand.render(cx).await) - div class="navbar-collapse" { (items) } + (items) }, // Barra con marca a la izquierda y botón a la derecha. bs::navbar::Layout::BrandLeft(brand) => { + @let id_content = util::join!(&id, "-content"); + (brand.render(cx).await) (button(cx, TOGGLE_COLLAPSE, &id_content)) div id=(&id_content) class="collapse navbar-collapse" { @@ -296,6 +287,8 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result { + @let id_content = util::join!(&id, "-content"); + (button(cx, TOGGLE_COLLAPSE, &id_content)) (brand.render(cx).await) div id=(&id_content) class="collapse navbar-collapse" { @@ -303,24 +296,36 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result { + // Barra cuyo contenido se muestra en un offcanvas, sin marca. + bs::navbar::Layout::Offcanvas(offcanvas) => { + @let id_content = offcanvas.id().unwrap_or_default(); + (button(cx, TOGGLE_OFFCANVAS, &id_content)) - (panel.render_panel(cx, &id_content, &items)) + @if let Some(oc) = offcanvas.get() { + (oc.render_offcanvas(cx, Some(navbar.items())).await) + } }, - // Barra con marca a la izquierda y contenido en un panel lateral. - bs::navbar::Layout::OffcanvasBrandLeft(brand, panel) => { + // Barra con marca a la izquierda y contenido en offcanvas. + bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => { + @let id_content = offcanvas.id().unwrap_or_default(); + (brand.render(cx).await) (button(cx, TOGGLE_OFFCANVAS, &id_content)) - (panel.render_panel(cx, &id_content, &items)) + @if let Some(oc) = offcanvas.get() { + (oc.render_offcanvas(cx, Some(navbar.items())).await) + } }, - // Barra con contenido en un panel lateral y marca a la derecha. - bs::navbar::Layout::OffcanvasBrandRight(brand, panel) => { + // Barra con contenido en offcanvas y marca a la derecha. + bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => { + @let id_content = offcanvas.id().unwrap_or_default(); + (button(cx, TOGGLE_OFFCANVAS, &id_content)) (brand.render(cx).await) - (panel.render_panel(cx, &id_content, &items)) + @if let Some(oc) = offcanvas.get() { + (oc.render_offcanvas(cx, Some(navbar.items())).await) + } }, } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar/panel.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/panel.rs deleted file mode 100644 index 1151dd25..00000000 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/panel.rs +++ /dev/null @@ -1,118 +0,0 @@ -use pagetop::prelude::*; - -use crate::LOCALES_BOOTSIER; -use crate::theme::*; - -/// Panel lateral deslizante (*offcanvas*) para una barra de navegación [`Navbar`]. -/// -/// Es el panel en el que se muestran los elementos de la barra cuando está colapsada. Se abre con -/// su botón de despliegue y sólo recoge lo que tiene sentido en una barra: el título, el borde -/// desde el que se despliega, la capa de fondo y el desplazamiento de la página. El resto lo decide -/// la propia barra ya que el contenido son sus elementos, el punto de corte a partir del cual se -/// muestra (definido en [`Navbar::with_expand()`]) y el identificador que se deriva del suyo. -/// -/// Para un panel lateral independiente, con contenido propio, se usa [`Offcanvas`]. -/// -/// # Ejemplo -/// -/// ```rust,no_run -/// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; -/// -/// let panel = bs::navbar::Panel::new() -/// .with_title(Lc::n("Main menu")) -/// .with_placement(bs::offcanvas::Placement::End) -/// .with_backdrop(bs::offcanvas::Backdrop::Static); -/// -/// let navbar = bs::Navbar::offcanvas(panel) -/// .with_item(bs::navbar::Item::nav( -/// bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")) -/// )); -/// ``` -/// -/// [`Navbar`]: crate::theme::bs::Navbar -/// [`Navbar::with_expand()`]: crate::theme::bs::Navbar::with_expand -/// [`Offcanvas`]: crate::theme::bs::Offcanvas -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Panel { - /// Devuelve el título del panel. - title: Lc, - /// Devuelve el borde desde el que se despliega el panel. - #[getters(copy)] - placement: bs::offcanvas::Placement, - /// Devuelve el comportamiento configurado para la capa de fondo. - #[getters(copy)] - backdrop: bs::offcanvas::Backdrop, - /// Indica si la página principal puede desplazarse mientras el panel está abierto. - #[getters(copy)] - body_scroll: bs::offcanvas::BodyScroll, -} - -#[builder_impl] -impl Panel { - /// Crea un panel sin título, que se despliega desde el borde inicial, con una capa de fondo y - /// sin permitir el desplazamiento de la página mientras está abierto. - pub fn new() -> Self { - Self::default() - } - - /// Establece el título del encabezado. - pub fn with_title(mut self, title: Lc) -> Self { - self.title = title; - self - } - - /// Indica desde qué borde de la ventana se despliega y se ancla el panel. - pub fn with_placement(mut self, placement: bs::offcanvas::Placement) -> Self { - self.placement = placement; - self - } - - /// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del - /// panel. - pub fn with_backdrop(mut self, backdrop: bs::offcanvas::Backdrop) -> Self { - self.backdrop = backdrop; - self - } - - /// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto. - pub fn with_body_scroll(mut self, scrolling: bs::offcanvas::BodyScroll) -> Self { - self.body_scroll = scrolling; - self - } - - // Renderiza el panel con `id` como identificador y `body` (los elementos de la barra ya - // renderizados) como contenido. Con `navbar-expand-{bp}` Bootstrap lo muestra en línea a partir - // del punto de corte de la barra, así que no lleva clase de punto de corte propia. - pub(super) fn render_panel(&self, cx: &Context, id: &str, body: &Markup) -> Markup { - let title = self.title().using(cx); - let id_label = util::join!(id, "-label"); - let id_target = util::join!("#", id); - let classes = util::join!("offcanvas ", self.placement().as_str()); - - html! { - div - id=(id) - class=(classes) - tabindex="-1" - data-bs-scroll=[self.body_scroll().opt_str()] - data-bs-backdrop=[self.backdrop().opt_str()] - aria-labelledby=[(!title.is_empty()).then_some(&id_label)] - { - div class="offcanvas-header" { - @if !title.is_empty() { - h5 id=(&id_label) class="offcanvas-title" { (title) } - } - button - type="button" - class="btn-close" - data-bs-dismiss="offcanvas" - data-bs-target=(id_target) - aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)] - {} - } - div class="offcanvas-body" { (body) } - } - } - } -} diff --git a/extensions/pagetop-bootsier/src/theme/bs/navbar/props.rs b/extensions/pagetop-bootsier/src/theme/bs/navbar/props.rs index d20f700d..d23c75c1 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/navbar/props.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/navbar/props.rs @@ -2,6 +2,8 @@ use pagetop::prelude::*; use crate::theme::*; +// **< Layout >************************************************************************************* + /// Representa los diferentes tipos de presentación de una barra de navegación /// [`Navbar`](crate::theme::bs::Navbar). #[derive(AutoDefault, Clone, Debug)] @@ -26,15 +28,74 @@ pub enum Layout { /// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha. BrandRight(Embed), - /// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con botón de despliegue - /// a la izquierda y sin marca de identidad. - Offcanvas(bs::navbar::Panel), + /// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la + /// izquierda y sin marca de identidad. + Offcanvas(Embed), - /// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con marca de identidad - /// a la izquierda y botón de despliegue a la derecha. - OffcanvasBrandLeft(Embed, bs::navbar::Panel), + /// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con marca de identidad a la + /// izquierda y botón de despliegue a la derecha. + OffcanvasBrandLeft(Embed, Embed), - /// Contenido en un [`Panel`](crate::theme::bs::navbar::Panel) lateral, con botón de despliegue - /// a la izquierda y marca de identidad a la derecha. - OffcanvasBrandRight(Embed, bs::navbar::Panel), + /// Contenido en [`Offcanvas`](crate::theme::bs::Offcanvas), con botón de despliegue a la + /// izquierda y marca de identidad a la derecha. + OffcanvasBrandRight(Embed, Embed), +} + +// **< Position >*********************************************************************************** + +/// Posición global de una barra de navegación [`Navbar`](crate::theme::bs::Navbar) en el documento. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +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, +} + +impl Position { + /// Devuelve la clase base asociada a la posición de la barra de navegación. + #[rustfmt::skip] + #[inline] + pub const fn as_str(self) -> &'static str { + match self { + Self::Static => "", + Self::FixedTop => "fixed-top", + Self::FixedBottom => "fixed-bottom", + Self::StickyTop => "sticky-top", + Self::StickyBottom => "sticky-bottom", + } + } + + /// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases. + #[inline] + pub fn push_to(self, classes: &mut String) { + let class = self.as_str(); + if class.is_empty() { + return; + } + if !classes.is_empty() { + classes.push(' '); + } + classes.push_str(class); + } + + /// Devuelve la clase asociada a la posición de la barra de navegación. + pub fn to_class(self) -> String { + let mut class = String::new(); + self.push_to(&mut class); + class + } } diff --git a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs index 729dc0cf..68709e80 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/offcanvas/component.rs @@ -48,19 +48,14 @@ pub struct Offcanvas { /// Devuelve el título del panel. title: Lc, /// Devuelve el punto de corte configurado para cambiar el comportamiento del panel. - #[getters(copy)] breakpoint: Breakpoint, /// Devuelve el comportamiento configurado para la capa de fondo. - #[getters(copy)] backdrop: bs::offcanvas::Backdrop, /// Indica si la página principal puede desplazarse mientras el panel está abierto. - #[getters(copy)] body_scroll: bs::offcanvas::BodyScroll, /// Devuelve la posición de inicio del panel. - #[getters(copy)] placement: bs::offcanvas::Placement, /// Devuelve el estado inicial del panel. - #[getters(copy)] visibility: bs::offcanvas::Visibility, /// Devuelve la lista de componentes (`children`) del panel. children: Children, @@ -83,7 +78,7 @@ impl Component for Offcanvas { // Clases CSS por defecto para el panel. self.alter_prop(PropsOp::prepend_classes({ let mut classes = String::new(); - push_breakpoint_class(cx, self.breakpoint(), &mut classes, "offcanvas", ""); + push_breakpoint_class(cx, *self.breakpoint(), &mut classes, "offcanvas", ""); self.placement().push_to(&mut classes); self.visibility().push_to(&mut classes); classes @@ -91,41 +86,7 @@ impl Component for Offcanvas { } async fn prepare(&self, cx: &mut Context) -> Result { - let body = self.children().render(cx).await; - if body.is_empty() { - return Ok(html! {}); - } - - // `setup()` garantiza que habrá un `id` antes de renderizar. - let id = self.id().unwrap(); - let id_label = util::join!(&id, "-label"); - let title = self.title().using(cx); - - Ok(html! { - div - (self.props().unpack(cx)) - tabindex="-1" - data-bs-scroll=[self.body_scroll().opt_str()] - data-bs-backdrop=[self.backdrop().opt_str()] - aria-labelledby=[(!title.is_empty()).then_some(&id_label)] - { - div class="offcanvas-header" { - @if !title.is_empty() { - h5 id=(&id_label) class="offcanvas-title" { (title) } - } - button - type="button" - class="btn-close" - data-bs-dismiss="offcanvas" - data-bs-target=(util::join!("#", &id)) - aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)] - {} - } - div class="offcanvas-body" { - (body) - } - } - }) + Ok(self.render_offcanvas(cx, None).await) } } @@ -196,4 +157,59 @@ impl Offcanvas { self.children.alter_child(op.into()); self } + + // **< Offcanvas HELPERS >********************************************************************** + + pub(crate) async fn render_offcanvas( + &self, + cx: &mut Context, + extra: Option<&Children>, + ) -> Markup { + let body = self.children().render(cx).await; + let body_extra = if let Some(c) = extra { + c.render(cx).await + } else { + html! {} + }; + if body.is_empty() && body_extra.is_empty() { + return html! {}; + } + + // `setup()` garantiza que habrá un `id` antes de renderizar. + let id = self.id().unwrap(); + let id_label = util::join!(id, "-label"); + let id_target = util::join!("#", id); + + let body_scroll = self.body_scroll().opt_str(); + let backdrop = self.backdrop().opt_str(); + + let title = self.title().using(cx); + + html! { + div + (self.props().unpack(cx)) + tabindex="-1" + data-bs-scroll=[body_scroll] + data-bs-backdrop=[backdrop] + aria-labelledby=(id_label) + { + div class="offcanvas-header" { + @if !title.is_empty() { + h5 id=(&id_label) class="offcanvas-title" { (title) } + } + button + type="button" + class="btn-close" + data-bs-dismiss="offcanvas" + data-bs-target=(id_target) + aria-label=[Lc::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)] + {} + } + div class="offcanvas-body" { + (body) + (body_extra) + } + } + } + } } diff --git a/extensions/pagetop-bootsier/tests/breakpoint_class.rs b/extensions/pagetop-bootsier/tests/breakpoint_class.rs index cdadbec0..1261c8e5 100644 --- a/extensions/pagetop-bootsier/tests/breakpoint_class.rs +++ b/extensions/pagetop-bootsier/tests/breakpoint_class.rs @@ -68,20 +68,20 @@ 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_toggle().with_item(item()); + let mut navbar = bs::Navbar::simple().with_item(item()); let html = navbar.render(&mut cx()).await.into_string(); assert!(html.contains("navbar-expand-md")); // The expansion class comes from the `Navbar` component only, so it is not duplicated. assert_eq!(html.matches("navbar-expand").count(), 1); - let mut navbar = bs::Navbar::simple_toggle() + 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")); // Bootstrap needs a plain `navbar-expand` to keep the bar always expanded. - let mut navbar = bs::Navbar::simple_toggle() + let mut navbar = bs::Navbar::simple() .with_expand(Breakpoint::Xs) .with_item(item()); let html = navbar.render(&mut cx()).await.into_string(); @@ -89,37 +89,6 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() { assert!(!html.contains("navbar-expand-")); } -#[pagetop::test] -async fn navbar_without_toggle_ignores_the_breakpoint() { - let item = - || bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/"))); - let brand = || Brand::new().with_title(Lc::n("PageTop")); - - // With nothing to collapse, Bootstrap would still stack the menu below the breakpoint. - let mut navbar = bs::Navbar::simple() - .with_expand(Breakpoint::Lg) - .with_item(item()); - let html = navbar.render(&mut cx()).await.into_string(); - assert!(html.contains("navbar-expand")); - assert!(!html.contains("navbar-expand-")); - - let mut navbar = bs::Navbar::simple_brand_left(brand()) - .with_expand(Breakpoint::Lg) - .with_item(item()); - let html = navbar.render(&mut cx()).await.into_string(); - assert!(html.contains("navbar-expand")); - assert!(!html.contains("navbar-expand-")); - // The menu stays next to the brand instead of being pushed to the opposite end. - assert!(html.contains(r#"