✨ (base): Implementa Navbar con funcionalidad base
Añade `Navbar::with_position()` y `navbar::Position` al núcleo, con las clases de Basic para barras fijas y pegajosas, y `navbar::Item::form()` para mostrar un formulario dentro de la barra. Las barras sin botón de despliegue (`Simple` y `SimpleBrandLeft`) ya no colapsan: el punto de corte no les afecta y su menú se mantiene en línea, con salto de línea si no cabe. En Bootsier usan `navbar-expand` y `SimpleBrandLeft` agrupa los contenidos junto a la marca.
This commit is contained in:
parent
f8f023b513
commit
c47724bb68
14 changed files with 1298 additions and 209 deletions
|
|
@ -268,20 +268,20 @@ body {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 576px) {
|
@media (width >= 576px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm {
|
.container-sm {
|
||||||
max-width: 540px;
|
max-width: 540px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
@media (width >= 768px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-md {
|
.container-md {
|
||||||
max-width: 720px;
|
max-width: 720px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 992px) {
|
@media (width >= 992px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-md,
|
.container-md,
|
||||||
|
|
@ -289,7 +289,7 @@ body {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1200px) {
|
@media (width >= 1200px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-md,
|
.container-md,
|
||||||
|
|
@ -298,7 +298,7 @@ body {
|
||||||
max-width: 1140px;
|
max-width: 1140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1400px) {
|
@media (width >= 1400px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-md,
|
.container-md,
|
||||||
|
|
@ -308,7 +308,7 @@ body {
|
||||||
max-width: 1320px;
|
max-width: 1320px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1920px) {
|
@media (width >= 1920px) {
|
||||||
.container,
|
.container,
|
||||||
.container-sm,
|
.container-sm,
|
||||||
.container-md,
|
.container-md,
|
||||||
|
|
@ -687,6 +687,13 @@ input:disabled + label {
|
||||||
color: var(--val-color--text--muted);
|
color: var(--val-color--text--muted);
|
||||||
pointer-events: none;
|
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 {
|
.nav-start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
@ -737,17 +744,64 @@ input:disabled + label {
|
||||||
content: "\2630";
|
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 {
|
.navbar-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex: 1 1 auto;
|
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 {
|
.navbar-text {
|
||||||
color: var(--val-color--text--muted);
|
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}`
|
* 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
|
* class, added by the `Navbar` component. `navbar-expand` alone (a breakpoint with no minimum
|
||||||
|
|
@ -757,16 +811,17 @@ input:disabled + label {
|
||||||
*
|
*
|
||||||
* While collapsed, the `.nav` items are stacked in a column, so a dropdown menu inside it is laid
|
* 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.
|
* 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 (max-width: 575.98px) {
|
@media (width < 576px) {
|
||||||
.navbar-expand-sm {
|
.navbar-expand-sm {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-sm:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-sm:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-sm:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-sm:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -775,26 +830,26 @@ input:disabled + label {
|
||||||
.navbar-expand-sm:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-sm:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-sm .nav {
|
.navbar-expand-sm:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-sm .nav .dropdown-menu {
|
.navbar-expand-sm:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (width < 768px) {
|
||||||
.navbar-expand-md {
|
.navbar-expand-md {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-md:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-md:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-md:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-md:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -803,26 +858,26 @@ input:disabled + label {
|
||||||
.navbar-expand-md:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-md:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-md .nav {
|
.navbar-expand-md:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-md .nav .dropdown-menu {
|
.navbar-expand-md:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
@media (width < 992px) {
|
||||||
.navbar-expand-lg {
|
.navbar-expand-lg {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-lg:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-lg:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-lg:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-lg:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -831,26 +886,26 @@ input:disabled + label {
|
||||||
.navbar-expand-lg:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-lg:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-lg .nav {
|
.navbar-expand-lg:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-lg .nav .dropdown-menu {
|
.navbar-expand-lg:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1199.98px) {
|
@media (width < 1200px) {
|
||||||
.navbar-expand-xl {
|
.navbar-expand-xl {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-xl:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-xl:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-xl:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-xl:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -859,26 +914,26 @@ input:disabled + label {
|
||||||
.navbar-expand-xl:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-xl:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-xl .nav {
|
.navbar-expand-xl:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-xl .nav .dropdown-menu {
|
.navbar-expand-xl:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1399.98px) {
|
@media (width < 1400px) {
|
||||||
.navbar-expand-xxl {
|
.navbar-expand-xxl {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxl:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-xxl:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-xxl:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-xxl:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -887,26 +942,26 @@ input:disabled + label {
|
||||||
.navbar-expand-xxl:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-xxl:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxl .nav {
|
.navbar-expand-xxl:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxl .nav .dropdown-menu {
|
.navbar-expand-xxl:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1919.98px) {
|
@media (width < 1920px) {
|
||||||
.navbar-expand-xxxl {
|
.navbar-expand-xxxl {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-toggle {
|
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-toggle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-content {
|
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-content {
|
||||||
|
order: 1;
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
@ -915,12 +970,12 @@ input:disabled + label {
|
||||||
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-content.show {
|
.navbar-expand-xxxl:has(.navbar-toggle) .navbar-content.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxxl .nav {
|
.navbar-expand-xxxl:has(.navbar-toggle) .nav {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.navbar-expand-xxxl .nav .dropdown-menu {
|
.navbar-expand-xxxl:has(.navbar-toggle) .nav .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
|
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_label = Label
|
||||||
menus_item_link = Link
|
menus_item_link = Link
|
||||||
menus_item_blank = External link
|
menus_item_blank = External link
|
||||||
menus_item_disabled = Disabled link
|
menus_item_disabled = Disabled link
|
||||||
|
menus_item_action = Action
|
||||||
|
menus_item_another = Another action
|
||||||
|
menus_item_something = Something else
|
||||||
|
|
||||||
menus_test_title = Dropdown
|
menus_test_title = Dropdown
|
||||||
|
|
||||||
|
|
@ -21,3 +44,66 @@ menus_plugin_cache = Rust Plugin Cache
|
||||||
|
|
||||||
menus_item_sign_up = Sign up
|
menus_item_sign_up = Sign up
|
||||||
menus_item_login = Login
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
|
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_label = Etiqueta
|
||||||
menus_item_link = Enlace
|
menus_item_link = Enlace
|
||||||
menus_item_blank = Externo
|
menus_item_blank = Externo
|
||||||
menus_item_disabled = Deshabilitado
|
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
|
menus_test_title = Desplegable
|
||||||
|
|
||||||
|
|
@ -21,3 +44,66 @@ menus_plugin_cache = Plugin Rust de caché
|
||||||
|
|
||||||
menus_item_sign_up = Registrarse
|
menus_item_sign_up = Registrarse
|
||||||
menus_item_login = Iniciar sesión
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,778 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
include_locales!(LOC from "examples/locale");
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct SuperMenu;
|
struct NavbarMenus;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Extension for SuperMenu {
|
impl Extension for NavbarMenus {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
&pagetop_aliner::Aliner,
|
|
||||||
&pagetop_bootsier::Bootsier,
|
|
||||||
&pagetop::base::extension::Welcome,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn initialize(&self) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
let navbar_menu = Navbar::brand_left(Brand::new())
|
router.route("/", web::get(navbar_menus))
|
||||||
.with_expand(Breakpoint::Lg)
|
}
|
||||||
.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",
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
|
|
||||||
InRegion::Global(&CoreRegions::Header).add(
|
async fn navbar_menus(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Container::new()
|
let page = Page::new(request)
|
||||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
.with_assets(demo_title_styles())
|
||||||
.with_child(navbar_menu),
|
.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::<Route>),
|
||||||
|
"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<PropsOp>, &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())
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< 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,
|
||||||
|
Container::new()
|
||||||
|
.with_width(container::Width::Fluid)
|
||||||
|
.with_prop(PropsOp::add_classes("menus-demo-frame"))
|
||||||
|
.with_child(navbar(position))
|
||||||
|
.with_child(filler()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::Offcanvas::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,
|
||||||
|
)),
|
||||||
|
"Navbar::offcanvas(Offcanvas::new().with_placement(offcanvas::Placement::Start))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_brand_left",
|
||||||
|
Navbar::offcanvas_brand_left(
|
||||||
|
demo_brand(),
|
||||||
|
panel(
|
||||||
|
bs::offcanvas::Placement::End,
|
||||||
|
bs::offcanvas::Backdrop::Enabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"Navbar::offcanvas_brand_left(brand, Offcanvas::new().with_placement(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, ",
|
||||||
|
"Offcanvas::new().with_backdrop(Backdrop::Disabled))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_dark",
|
||||||
|
Navbar::offcanvas_brand_left(
|
||||||
|
demo_brand(),
|
||||||
|
panel(
|
||||||
|
bs::offcanvas::Placement::End,
|
||||||
|
bs::offcanvas::Backdrop::Static,
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::set("data-bs-theme", "dark")),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)))
|
||||||
|
.with_prop(PropsOp::set("data-bs-theme", "dark")),
|
||||||
|
concat!(
|
||||||
|
"Navbar::offcanvas_brand_left(brand, Offcanvas::new()",
|
||||||
|
".with_backdrop(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<ChildOp>) -> 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]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&SuperMenu).await.run().await
|
Application::prepare(&NavbarMenus).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::{Layout, Position};
|
pub use props::Layout;
|
||||||
|
|
||||||
pub use super::Brand;
|
pub use super::Brand;
|
||||||
|
|
||||||
pub use pagetop::base::component::Navbar;
|
pub use pagetop::base::component::Navbar;
|
||||||
pub use pagetop::base::component::navbar::Item;
|
pub use pagetop::base::component::navbar::{Item, Position};
|
||||||
|
|
||||||
mod component;
|
mod component;
|
||||||
pub use component::NavbarBootsier;
|
pub use component::NavbarBootsier;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ const TOGGLE_COLLAPSE: &str = "collapse";
|
||||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||||
|
|
||||||
const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
|
const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
|
||||||
const EXTRA_POSITION: &str = "bootsier.navbar.position";
|
|
||||||
|
|
||||||
/// Extensión de Bootsier para [`Navbar`].
|
/// Extensión de Bootsier para [`Navbar`].
|
||||||
///
|
///
|
||||||
|
|
@ -151,9 +150,6 @@ pub trait NavbarBootsier {
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
|
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
|
||||||
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self;
|
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]
|
#[builder_impl]
|
||||||
|
|
@ -184,23 +180,33 @@ impl NavbarBootsier for Navbar {
|
||||||
));
|
));
|
||||||
navbar
|
navbar
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_position(mut self, position: bs::navbar::Position) -> Self {
|
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Navbar SETUP >*******************************************************************************
|
// **< Navbar SETUP >*******************************************************************************
|
||||||
|
|
||||||
pub(crate) fn setup(navbar: &mut Navbar) {
|
pub(crate) fn setup(navbar: &mut Navbar) {
|
||||||
let position = 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 `Offcanvas`)
|
||||||
|
// siempre llevan botón; de las de base sólo `Simple` y `SimpleBrandLeft` no lo llevan.
|
||||||
|
let has_toggle = navbar
|
||||||
.props()
|
.props()
|
||||||
.extra_or(EXTRA_POSITION, bs::navbar::Position::default());
|
.extra::<bs::navbar::Layout>(EXTRA_LAYOUT)
|
||||||
let mut classes = String::new();
|
.is_ok()
|
||||||
position.push_to(&mut classes);
|
|| !matches!(
|
||||||
if !classes.is_empty() {
|
navbar.layout(),
|
||||||
navbar.alter_prop(PropsOp::add_classes(classes));
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,10 +274,13 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con marca a la izquierda, siempre visible.
|
// 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.
|
||||||
bs::navbar::Layout::SimpleBrandLeft(brand) => {
|
bs::navbar::Layout::SimpleBrandLeft(brand) => {
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
(items)
|
div class="navbar-collapse" { (items) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con marca a la izquierda y botón a la derecha.
|
// Barra con marca a la izquierda y botón a la derecha.
|
||||||
|
|
@ -298,32 +307,41 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
||||||
|
|
||||||
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
||||||
bs::navbar::Layout::Offcanvas(offcanvas) => {
|
bs::navbar::Layout::Offcanvas(offcanvas) => {
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||||
|
@let id_content = offcanvas
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
@if let Some(oc) = offcanvas.get() {
|
@if let Some(oc) = &offcanvas {
|
||||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con marca a la izquierda y contenido en offcanvas.
|
// Barra con marca a la izquierda y contenido en offcanvas.
|
||||||
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||||
|
@let id_content = offcanvas
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||||
|
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
@if let Some(oc) = offcanvas.get() {
|
@if let Some(oc) = &offcanvas {
|
||||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Barra con contenido en offcanvas y marca a la derecha.
|
// Barra con contenido en offcanvas y marca a la derecha.
|
||||||
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
@let offcanvas = setup_offcanvas(&offcanvas, cx);
|
||||||
|
@let id_content = offcanvas
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|oc| oc.id()).unwrap_or_default();
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||||
(brand.render(cx).await)
|
(brand.render(cx).await)
|
||||||
@if let Some(oc) = offcanvas.get() {
|
@if let Some(oc) = &offcanvas {
|
||||||
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
(oc.render_offcanvas(cx, Some(navbar.items())).await)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -333,6 +351,16 @@ pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// El panel se renderiza con `render_offcanvas()` para inyectarle los contenidos de la barra, así
|
||||||
|
// que no pasa por el ciclo de vida normal. Se clona y se le aplica aquí su `setup()`; sin él no
|
||||||
|
// tendría `id` (el botón lo necesita para referenciarlo) ni las clases `offcanvas*` que lo ocultan
|
||||||
|
// y lo colocan.
|
||||||
|
fn setup_offcanvas(offcanvas: &Embed<bs::Offcanvas>, cx: &mut Context) -> Option<bs::Offcanvas> {
|
||||||
|
let mut oc = offcanvas.get()?.clone();
|
||||||
|
oc.setup(cx);
|
||||||
|
Some(oc)
|
||||||
|
}
|
||||||
|
|
||||||
// Traduce el `navbar::Layout` semántico de base (sin `Offcanvas`, sin `Position`/`expand`) a la
|
// Traduce el `navbar::Layout` semántico de base (sin `Offcanvas`, sin `Position`/`expand`) a la
|
||||||
// variante equivalente de `bs::navbar::Layout`, para las barras construidas con los constructores
|
// variante equivalente de `bs::navbar::Layout`, para las barras construidas con los constructores
|
||||||
// heredados de `Navbar` (`simple()`, `simple_toggle()`, `simple_brand_left()`, `brand_left()`,
|
// heredados de `Navbar` (`simple()`, `simple_toggle()`, `simple_brand_left()`, `brand_left()`,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
// **< Layout >*************************************************************************************
|
|
||||||
|
|
||||||
/// Representa los diferentes tipos de presentación de una barra de navegación
|
/// Representa los diferentes tipos de presentación de una barra de navegación
|
||||||
/// [`Navbar`](crate::theme::bs::Navbar).
|
/// [`Navbar`](crate::theme::bs::Navbar).
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
|
|
@ -40,62 +38,3 @@ pub enum Layout {
|
||||||
/// izquierda y marca de identidad a la derecha.
|
/// izquierda y marca de identidad a la derecha.
|
||||||
OffcanvasBrandRight(Embed<Brand>, Embed<bs::Offcanvas>),
|
OffcanvasBrandRight(Embed<Brand>, Embed<bs::Offcanvas>),
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -68,20 +68,20 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
|
||||||
let item =
|
let item =
|
||||||
|| bs::navbar::Item::nav(bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")));
|
|| 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 mut navbar = bs::Navbar::simple_toggle().with_item(item());
|
||||||
let html = navbar.render(&mut cx()).await.into_string();
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
assert!(html.contains("navbar-expand-md"));
|
assert!(html.contains("navbar-expand-md"));
|
||||||
// The expansion class comes from the `Navbar` component only, so it is not duplicated.
|
// The expansion class comes from the `Navbar` component only, so it is not duplicated.
|
||||||
assert_eq!(html.matches("navbar-expand").count(), 1);
|
assert_eq!(html.matches("navbar-expand").count(), 1);
|
||||||
|
|
||||||
let mut navbar = bs::Navbar::simple()
|
let mut navbar = bs::Navbar::simple_toggle()
|
||||||
.with_expand(Breakpoint::Xxxl)
|
.with_expand(Breakpoint::Xxxl)
|
||||||
.with_item(item());
|
.with_item(item());
|
||||||
let html = navbar.render(&mut cx()).await.into_string();
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
assert!(html.contains("navbar-expand-xxl"));
|
assert!(html.contains("navbar-expand-xxl"));
|
||||||
|
|
||||||
// Bootstrap needs a plain `navbar-expand` to keep the bar always expanded.
|
// Bootstrap needs a plain `navbar-expand` to keep the bar always expanded.
|
||||||
let mut navbar = bs::Navbar::simple()
|
let mut navbar = bs::Navbar::simple_toggle()
|
||||||
.with_expand(Breakpoint::Xs)
|
.with_expand(Breakpoint::Xs)
|
||||||
.with_item(item());
|
.with_item(item());
|
||||||
let html = navbar.render(&mut cx()).await.into_string();
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
|
|
@ -89,6 +89,37 @@ async fn navbar_expands_at_md_by_default_and_at_the_chosen_breakpoint() {
|
||||||
assert!(!html.contains("navbar-expand-"));
|
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#"<div class="navbar-collapse">"#));
|
||||||
|
|
||||||
|
// The layouts with a toggle keep their breakpoint.
|
||||||
|
let mut navbar = bs::Navbar::brand_left(brand())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(item());
|
||||||
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
|
assert!(html.contains("navbar-expand-lg"));
|
||||||
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn offcanvas_uses_either_the_plain_or_the_breakpoint_class() {
|
async fn offcanvas_uses_either_the_plain_or_the_breakpoint_class() {
|
||||||
// Bootstrap's `.offcanvas` is always fixed, so it must not accompany `.offcanvas-{bp}`.
|
// Bootstrap's `.offcanvas` is always fixed, so it must not accompany `.offcanvas-{bp}`.
|
||||||
|
|
|
||||||
62
extensions/pagetop-bootsier/tests/navbar_offcanvas.rs
Normal file
62
extensions/pagetop-bootsier/tests/navbar_offcanvas.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Verifies that a `Navbar` with its content in an `Offcanvas` renders a working panel: the toggle
|
||||||
|
// button must point to the `id` of the panel, and the panel must carry its own placement class.
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
|
fn cx() -> Context {
|
||||||
|
Context::default().with_theme(&pagetop_bootsier::Bootsier)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel(placement: bs::offcanvas::Placement) -> bs::Offcanvas {
|
||||||
|
bs::Offcanvas::new().with_placement(placement)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn offcanvas_button_points_to_the_panel_id() {
|
||||||
|
let brand = || Brand::new().with_title(Lc::n("PageTop"));
|
||||||
|
let navbars = [
|
||||||
|
bs::Navbar::offcanvas(panel(bs::offcanvas::Placement::Start)),
|
||||||
|
bs::Navbar::offcanvas_brand_left(brand(), panel(bs::offcanvas::Placement::Start)),
|
||||||
|
bs::Navbar::offcanvas_brand_right(brand(), panel(bs::offcanvas::Placement::Start)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for mut navbar in navbars {
|
||||||
|
navbar = navbar.with_item(bs::navbar::Item::nav(
|
||||||
|
bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")),
|
||||||
|
));
|
||||||
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
|
|
||||||
|
// The panel is rendered by the navbar, so the navbar must set its `id` up.
|
||||||
|
let target = html
|
||||||
|
.split(r##"data-bs-target="#"##)
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|rest| rest.split('"').next())
|
||||||
|
.unwrap_or_default();
|
||||||
|
assert!(!target.is_empty(), "the button has no target in {html}");
|
||||||
|
assert!(html.contains(&format!(r#"aria-controls="{target}""#)));
|
||||||
|
assert!(
|
||||||
|
html.contains(&format!(r#"id="{target}""#)),
|
||||||
|
"no panel with id `{target}` in {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn offcanvas_panel_gets_its_own_classes() {
|
||||||
|
for (placement, class) in [
|
||||||
|
(bs::offcanvas::Placement::Start, "offcanvas-start"),
|
||||||
|
(bs::offcanvas::Placement::End, "offcanvas-end"),
|
||||||
|
] {
|
||||||
|
let mut navbar = bs::Navbar::offcanvas(panel(placement)).with_item(bs::navbar::Item::nav(
|
||||||
|
bs::Nav::new().with_item(bs::nav::Item::link(Lc::n("Home"), "/")),
|
||||||
|
));
|
||||||
|
let html = navbar.render(&mut cx()).await.into_string();
|
||||||
|
|
||||||
|
// Without them Bootstrap would show the panel inline instead of hiding and placing it.
|
||||||
|
assert!(
|
||||||
|
html.contains(&format!(r#"class="offcanvas {class}""#)),
|
||||||
|
"missing `offcanvas {class}` in {html}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Definiciones para crear barras de navegación ([`Navbar`]) y sus elementos ([`Item`]).
|
//! Definiciones para crear barras de navegación ([`Navbar`]) y sus elementos ([`Item`]).
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::Layout;
|
pub use props::{Layout, Position};
|
||||||
|
|
||||||
mod component;
|
mod component;
|
||||||
pub use component::Navbar;
|
pub use component::Navbar;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ pub struct Navbar {
|
||||||
#[default(Breakpoint::Md)]
|
#[default(Breakpoint::Md)]
|
||||||
#[getters(copy)]
|
#[getters(copy)]
|
||||||
expand: Breakpoint,
|
expand: Breakpoint,
|
||||||
|
/// Devuelve la posición de la barra en el documento.
|
||||||
|
#[getters(copy)]
|
||||||
|
position: navbar::Position,
|
||||||
/// Devuelve la lista de contenidos.
|
/// Devuelve la lista de contenidos.
|
||||||
items: Children,
|
items: Children,
|
||||||
}
|
}
|
||||||
|
|
@ -115,6 +118,11 @@ impl Component for Navbar {
|
||||||
};
|
};
|
||||||
self.alter_prop(PropsOp::prepend_classes(class));
|
self.alter_prop(PropsOp::prepend_classes(class));
|
||||||
self.alter_prop(PropsOp::prepend_classes("navbar"));
|
self.alter_prop(PropsOp::prepend_classes("navbar"));
|
||||||
|
|
||||||
|
let position = self.position().as_str();
|
||||||
|
if !position.is_empty() {
|
||||||
|
self.alter_prop(PropsOp::add_classes(position));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
|
@ -237,11 +245,23 @@ impl Navbar {
|
||||||
/// Por debajo de ese punto de corte, en las disposiciones con botón de despliegue el contenido
|
/// Por debajo de ese punto de corte, en las disposiciones con botón de despliegue el contenido
|
||||||
/// queda oculto tras el botón. Con [`Breakpoint::Xs`] la barra nunca colapsa. Por defecto es
|
/// queda oculto tras el botón. Con [`Breakpoint::Xs`] la barra nunca colapsa. Por defecto es
|
||||||
/// [`Breakpoint::Md`].
|
/// [`Breakpoint::Md`].
|
||||||
|
///
|
||||||
|
/// Sólo tiene efecto en las disposiciones con botón de despliegue: en
|
||||||
|
/// [`navbar::Layout::Simple`] y [`navbar::Layout::SimpleBrandLeft`] no hay nada que colapsar y
|
||||||
|
/// la barra siempre se muestra en línea.
|
||||||
pub fn with_expand(mut self, bp: Breakpoint) -> Self {
|
pub fn with_expand(mut self, bp: Breakpoint) -> Self {
|
||||||
self.expand = bp;
|
self.expand = bp;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Define dónde se mostrará la barra de navegación dentro del documento.
|
||||||
|
///
|
||||||
|
/// Por defecto es [`navbar::Position::Static`], la barra fluye con el documento.
|
||||||
|
pub fn with_position(mut self, position: navbar::Position) -> Self {
|
||||||
|
self.position = position;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||||
/// barra con una operación [`TypedOp`].
|
/// barra con una operación [`TypedOp`].
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ pub enum Item {
|
||||||
Brand(Embed<Brand>),
|
Brand(Embed<Brand>),
|
||||||
/// Representa un menú de navegación [`Nav`].
|
/// Representa un menú de navegación [`Nav`].
|
||||||
Nav(Embed<Nav>),
|
Nav(Embed<Nav>),
|
||||||
|
/// Representa un formulario [`Form`] (búsqueda, botones...), renderizado tal cual.
|
||||||
|
Form(Embed<Form>),
|
||||||
/// Representa un *texto localizado* libre.
|
/// Representa un *texto localizado* libre.
|
||||||
Text(Lc),
|
Text(Lc),
|
||||||
}
|
}
|
||||||
|
|
@ -33,6 +35,7 @@ impl Component for Item {
|
||||||
Self::Void => None,
|
Self::Void => None,
|
||||||
Self::Brand(brand) => brand.id(),
|
Self::Brand(brand) => brand.id(),
|
||||||
Self::Nav(nav) => nav.id(),
|
Self::Nav(nav) => nav.id(),
|
||||||
|
Self::Form(form) => form.id(),
|
||||||
Self::Text(_) => None,
|
Self::Text(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +56,7 @@ impl Component for Item {
|
||||||
Self::Void => html! {},
|
Self::Void => html! {},
|
||||||
Self::Brand(brand) => html! { (brand.render(cx).await) },
|
Self::Brand(brand) => html! { (brand.render(cx).await) },
|
||||||
Self::Nav(nav) => html! { (nav.render(cx).await) },
|
Self::Nav(nav) => html! { (nav.render(cx).await) },
|
||||||
|
Self::Form(form) => html! { (form.render(cx).await) },
|
||||||
Self::Text(text) => html! {
|
Self::Text(text) => html! {
|
||||||
span class="navbar-text" {
|
span class="navbar-text" {
|
||||||
(text.using(cx))
|
(text.using(cx))
|
||||||
|
|
@ -77,6 +81,27 @@ impl Item {
|
||||||
Self::Nav(Embed::with(item))
|
Self::Nav(Embed::with(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea un elemento de tipo [`Form`] para añadir al contenido de [`Navbar`](super::Navbar).
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// Formulario de búsqueda dentro de la barra:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let search = Form::new().with_action("/search").with_child(
|
||||||
|
/// Flex::new()
|
||||||
|
/// .with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
|
/// .with_child(form::input::Field::search().with_name("q"))
|
||||||
|
/// .with_child(Button::submit(Lc::n("Search"))),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// let navbar = Navbar::simple().with_item(navbar::Item::form(search));
|
||||||
|
/// ```
|
||||||
|
pub fn form(form: Form) -> Self {
|
||||||
|
Self::Form(Embed::with(form))
|
||||||
|
}
|
||||||
|
|
||||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||||
pub fn text(item: Lc) -> Self {
|
pub fn text(item: Lc) -> Self {
|
||||||
Self::Text(item)
|
Self::Text(item)
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use crate::prelude::*;
|
||||||
/// [`Navbar`](super::Navbar).
|
/// [`Navbar`](super::Navbar).
|
||||||
///
|
///
|
||||||
/// Sólo recoge las combinaciones de marca y botón de despliegue independientes de cualquier
|
/// Sólo recoge las combinaciones de marca y botón de despliegue independientes de cualquier
|
||||||
/// framework CSS. Un tema puede definir su propia variante de disposición (posiciones fijas,
|
/// framework CSS. Un tema puede definir su propia variante de disposición (contenido en un panel
|
||||||
/// contenido en un panel lateral...) con su propio tipo, sin depender de éste.
|
/// lateral...) con su propio tipo, sin depender de éste.
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
pub enum Layout {
|
pub enum Layout {
|
||||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||||
|
|
@ -30,3 +30,39 @@ pub enum Layout {
|
||||||
/// Barra con botón de despliegue y marca de identidad, en ese orden.
|
/// Barra con botón de despliegue y marca de identidad, en ese orden.
|
||||||
BrandRight(Embed<Brand>),
|
BrandRight(Embed<Brand>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< Position >***********************************************************************************
|
||||||
|
|
||||||
|
/// Posición de una barra de navegación [`Navbar`](super::Navbar) en el documento.
|
||||||
|
///
|
||||||
|
/// Las variantes `Fixed*` sacan la barra del flujo del documento, por lo que pueden tapar el
|
||||||
|
/// contenido que queda debajo: hay que reservarle espacio.
|
||||||
|
#[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.
|
||||||
|
FixedTop,
|
||||||
|
/// Barra fijada en la parte inferior, siempre visible.
|
||||||
|
FixedBottom,
|
||||||
|
/// Barra que se fija en la parte superior al hacer *scroll*.
|
||||||
|
StickyTop,
|
||||||
|
/// Barra que se fija en la parte inferior al hacer *scroll*.
|
||||||
|
StickyBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
/// Devuelve la clase CSS asociada a la posición, o una cadena vacía si es `Static`.
|
||||||
|
#[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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,23 @@ async fn is_not_rendered_when_empty() {
|
||||||
assert!(html.is_empty());
|
assert!(html.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< navbar::Item::form >*************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn item_form_renders_the_form() {
|
||||||
|
let mut navbar = Navbar::simple().with_item(navbar::Item::form(
|
||||||
|
Form::new()
|
||||||
|
.with_id("search")
|
||||||
|
.with_action("/search")
|
||||||
|
.with_child(Button::submit(Lc::n("Go"))),
|
||||||
|
));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(r#"<form"#));
|
||||||
|
assert!(html.contains(r#"id="search""#));
|
||||||
|
assert!(html.contains("Go"));
|
||||||
|
}
|
||||||
|
|
||||||
// **< Navbar + FlexItem::push_end >****************************************************************
|
// **< Navbar + FlexItem::push_end >****************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
@ -27,6 +44,36 @@ async fn push_end_adds_an_automatic_start_margin() {
|
||||||
assert!(assets.contains("_flex-item-offset_auto_{margin-inline-start:auto}"));
|
assert!(assets.contains("_flex-item-offset_auto_{margin-inline-start:auto}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **< Navbar::with_position >**********************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn position_is_static_by_default_and_adds_no_class() {
|
||||||
|
assert_eq!(Navbar::simple().position(), navbar::Position::Static);
|
||||||
|
|
||||||
|
let mut navbar = Navbar::simple().with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(!html.contains("fixed-"));
|
||||||
|
assert!(!html.contains("sticky-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn position_adds_its_class() {
|
||||||
|
for (position, class) in [
|
||||||
|
(navbar::Position::FixedTop, "fixed-top"),
|
||||||
|
(navbar::Position::FixedBottom, "fixed-bottom"),
|
||||||
|
(navbar::Position::StickyTop, "sticky-top"),
|
||||||
|
(navbar::Position::StickyBottom, "sticky-bottom"),
|
||||||
|
] {
|
||||||
|
let mut navbar = Navbar::simple()
|
||||||
|
.with_position(position)
|
||||||
|
.with_item(navbar::Item::nav(one_link_nav()));
|
||||||
|
let html = navbar.render(&mut Context::default()).await.into_string();
|
||||||
|
|
||||||
|
assert!(html.contains(class), "missing `{class}` in {html}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// **< Navbar::with_expand >************************************************************************
|
// **< Navbar::with_expand >************************************************************************
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue