Compare commits

..

No commits in common. "13fbdbe007dbe025467283537315ceb289f19c8d" and "93804721e161e5e1d52800df9147b55cdd02eeb8" have entirely different histories.

14 changed files with 1036 additions and 150 deletions

View file

@ -1,101 +0,0 @@
use pagetop::prelude::*;
use pagetop_bootsier::prelude::*;
struct SuperMenu;
impl Extension for SuperMenu {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
}
fn initialize(&self) {
let home_path = |cx: &Context| match cx.langid().language.as_str() {
"en" => "/en",
_ => "/",
};
let navbar_menu = Navbar::brand_left(navbar::Brand::new().with_path(Some(home_path)))
.with_expand(BreakPoint::LG)
.add_item(navbar::Item::nav(
Nav::new()
.add_item(nav::Item::link(
L10n::l("sample_menus_item_link"),
home_path,
))
.add_item(nav::Item::link_blank(
L10n::l("sample_menus_item_blank"),
|_| "https://docs.rs/pagetop",
))
.add_item(nav::Item::dropdown(
Dropdown::new()
.with_title(L10n::l("sample_menus_test_title"))
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_dev_getting_started"),
|_| "/dev/getting-started",
))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_dev_guides"),
|_| "/dev/guides",
))
.add_item(dropdown::Item::link_blank(
L10n::l("sample_menus_dev_forum"),
|_| "https://forum.example.dev",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_sdk_rust"),
|_| "/dev/sdks/rust",
))
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
"/dev/sdks/js"
}))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_sdk_python"),
|_| "/dev/sdks/python",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::header(L10n::l(
"sample_menus_plugin_header",
)))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_plugin_auth"),
|_| "/dev/sdks/rust/plugins/auth",
))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_plugin_cache"),
|_| "/dev/sdks/rust/plugins/cache",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
.add_item(dropdown::Item::link_disabled(
L10n::l("sample_menus_item_disabled"),
|_| "#",
)),
))
.add_item(nav::Item::link_disabled(
L10n::l("sample_menus_item_disabled"),
|_| "#",
)),
))
.add_item(navbar::Item::nav(
Nav::new()
.add_item(nav::Item::link(
L10n::l("sample_menus_item_sign_up"),
|_| "/auth/sign-up",
))
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
"/auth/login"
})),
));
InRegion::Key("header").add(Child::with(navbar_menu));
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&SuperMenu).run()?.await
}

View file

@ -13,7 +13,7 @@
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
//! .with_visibility(offcanvas::Visibility::Default)
//! .add_child(Dropdown::new()
//! .with_title(L10n::n("Menu"))
//! .with_button_title(L10n::n("Menu"))
//! .add_item(dropdown::Item::label(L10n::n("Label")))
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))

View file

@ -62,3 +62,5 @@ pub use poweredby::PoweredBy;
mod icon;
pub use icon::{Icon, IconKind};
pub mod menu;

View file

@ -0,0 +1,17 @@
mod menu_menu;
pub use menu_menu::Menu;
mod item;
pub use item::{Item, ItemKind};
mod submenu;
pub use submenu::Submenu;
mod megamenu;
pub use megamenu::Megamenu;
mod group;
pub use group::Group;
mod element;
pub use element::{Element, ElementType};

View file

@ -0,0 +1,56 @@
use crate::prelude::*;
type Content = Typed<Html>;
type SubmenuItems = Typed<menu::Submenu>;
#[derive(AutoDefault)]
pub enum ElementType {
#[default]
Void,
Html(Content),
Submenu(SubmenuItems),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Element {
element_type: ElementType,
}
impl Component for Element {
fn new() -> Self {
Element::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.element_type() {
ElementType::Void => PrepareMarkup::None,
ElementType::Html(content) => PrepareMarkup::With(html! {
(content.render(cx))
}),
ElementType::Submenu(submenu) => PrepareMarkup::With(html! {
(submenu.render(cx))
}),
}
}
}
impl Element {
pub fn html(content: Html) -> Self {
Element {
element_type: ElementType::Html(Content::with(content)),
}
}
pub fn submenu(submenu: menu::Submenu) -> Self {
Element {
element_type: ElementType::Submenu(SubmenuItems::with(submenu)),
}
}
// **< Element GETTERS >************************************************************************
pub fn element_type(&self) -> &ElementType {
&self.element_type
}
}

View file

@ -0,0 +1,58 @@
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Group {
id : AttrId,
elements: Children,
}
impl Component for Group {
fn new() -> Self {
Group::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="menu__group" {
(self.elements().render(cx))
}
})
}
}
impl Group {
// **< Group BUILDER >**************************************************************************
/// Establece el identificador único (`id`) del grupo.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Añade un nuevo elemento al menú.
pub fn add_element(mut self, element: menu::Element) -> Self {
self.elements
.alter_typed(TypedOp::Add(Typed::with(element)));
self
}
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_elements(mut self, op: TypedOp<menu::Element>) -> Self {
self.elements.alter_typed(op);
self
}
// **< Group GETTERS >**************************************************************************
/// Devuelve la lista de elementos (`children`) del grupo.
pub fn elements(&self) -> &Children {
&self.elements
}
}

View file

@ -0,0 +1,185 @@
use crate::prelude::*;
type Label = L10n;
type Content = Typed<Html>;
type SubmenuItems = Typed<menu::Submenu>;
type MegamenuGroups = Typed<menu::Megamenu>;
#[derive(AutoDefault)]
pub enum ItemKind {
#[default]
Void,
Label(Label),
Link(Label, FnPathByContext),
LinkBlank(Label, FnPathByContext),
Html(Content),
Submenu(Label, SubmenuItems),
Megamenu(Label, MegamenuGroups),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_kind : ItemKind,
description: AttrL10n,
left_icon : Typed<Icon>,
right_icon : Typed<Icon>,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description = self.description().lookup(cx);
let left_icon = self.left_icon().render(cx);
let right_icon = self.right_icon().render(cx);
match self.item_kind() {
ItemKind::Void => PrepareMarkup::None,
ItemKind::Label(label) => PrepareMarkup::With(html! {
li class="menu__item menu__item--label" {
span title=[description] {
(left_icon)
span class="menu__label" { (label.using(cx)) }
(right_icon)
}
}
}),
ItemKind::Link(label, path) => PrepareMarkup::With(html! {
li class="menu__item menu__item--link" {
a class="menu__link" href=(path(cx)) title=[description] {
(left_icon)
span class="menu__label" { (label.using(cx)) }
(right_icon)
}
}
}),
ItemKind::LinkBlank(label, path) => PrepareMarkup::With(html! {
li class="menu__item menu__item--link" {
a class="menu__link" href=(path(cx)) title=[description] target="_blank" {
(left_icon)
span class="menu__label" { (label.using(cx)) }
(right_icon)
}
}
}),
ItemKind::Html(content) => PrepareMarkup::With(html! {
li class="menu__item menu__item--html" {
(content.render(cx))
}
}),
ItemKind::Submenu(label, submenu) => PrepareMarkup::With(html! {
li class="menu__item menu__item--children" {
button type="button" class="menu__link" title=[description] {
(left_icon)
span class="menu__label" { (label.using(cx)) }
(Icon::svg(html! {
path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708" {}
}).render(cx))
}
div class="menu__children menu__children--submenu" {
(submenu.render(cx))
}
}
}),
ItemKind::Megamenu(label, megamenu) => PrepareMarkup::With(html! {
li class="menu__item menu__item--children" {
button type="button" class="menu__link" title=[description] {
(left_icon)
span class="menu__label" { (label.using(cx)) }
(Icon::svg(html! {
path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708" {}
}).render(cx))
}
div class="menu__children menu__children--mega" {
(megamenu.render(cx))
}
}
}),
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_kind: ItemKind::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::LinkBlank(label, path),
..Default::default()
}
}
pub fn html(content: Html) -> Self {
Item {
item_kind: ItemKind::Html(Content::with(content)),
..Default::default()
}
}
pub fn submenu(label: L10n, submenu: menu::Submenu) -> Self {
Item {
item_kind: ItemKind::Submenu(label, SubmenuItems::with(submenu)),
..Default::default()
}
}
pub fn megamenu(label: L10n, megamenu: menu::Megamenu) -> Self {
Item {
item_kind: ItemKind::Megamenu(label, MegamenuGroups::with(megamenu)),
..Default::default()
}
}
// **< Item BUILDER >***************************************************************************
#[builder_fn]
pub fn with_description(mut self, text: L10n) -> Self {
self.description.alter_value(text);
self
}
#[builder_fn]
pub fn with_left_icon<I: Into<Icon>>(mut self, icon: Option<I>) -> Self {
self.left_icon.alter_component(icon.map(Into::into));
self
}
#[builder_fn]
pub fn with_right_icon<I: Into<Icon>>(mut self, icon: Option<I>) -> Self {
self.right_icon.alter_component(icon.map(Into::into));
self
}
// **< Item GETTERS >***************************************************************************
pub fn item_kind(&self) -> &ItemKind {
&self.item_kind
}
pub fn description(&self) -> &AttrL10n {
&self.description
}
pub fn left_icon(&self) -> &Typed<Icon> {
&self.left_icon
}
pub fn right_icon(&self) -> &Typed<Icon> {
&self.right_icon
}
}

View file

@ -0,0 +1,57 @@
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Megamenu {
id : AttrId,
groups: Children,
}
impl Component for Megamenu {
fn new() -> Self {
Megamenu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="menu__mega" {
(self.groups().render(cx))
}
})
}
}
impl Megamenu {
// **< Megamenu BUILDER >***********************************************************************
/// Establece el identificador único (`id`) del megamenú.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Añade un nuevo grupo al menú.
pub fn add_group(mut self, group: menu::Group) -> Self {
self.groups.alter_typed(TypedOp::Add(Typed::with(group)));
self
}
/// Modifica la lista de grupos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_groups(mut self, op: TypedOp<menu::Group>) -> Self {
self.groups.alter_typed(op);
self
}
// **< Megamenu GETTERS >***********************************************************************
/// Devuelve la lista de grupos (`children`) del megamenú.
pub fn groups(&self) -> &Children {
&self.groups
}
}

View file

@ -0,0 +1,108 @@
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Menu {
id : AttrId,
classes: AttrClasses,
items : Children,
}
impl Component for Menu {
fn new() -> Self {
Menu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "menu");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
// cx.set_param::<bool>(PARAM_BASE_INCLUDE_MENU_ASSETS, &true);
// cx.set_param::<bool>(PARAM_BASE_INCLUDE_ICONS, &true);
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
div class="menu__wrapper" {
div class="menu__panel" {
div class="menu__overlay" {}
nav class="menu__nav" {
div class="menu__header" {
button type="button" class="menu__back" {
(Icon::svg(html! {
path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0" {}
}).render(cx))
}
div class="menu__title" {}
button type="button" class="menu__close" {
(Icon::svg(html! {
path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" {}
}).render(cx))
}
}
ul class="menu__list" {
(self.items().render(cx))
}
}
}
button
type="button"
class="menu__trigger"
title=[L10n::l("menu_toggle").lookup(cx)]
{
(Icon::svg(html! {
path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5" {}
}).render(cx))
}
}
}
})
}
}
impl Menu {
// **< Menu BUILDER >***************************************************************************
/// Establece el identificador único (`id`) del menú.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al menú.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Añade un nuevo ítem al menú.
pub fn add_item(mut self, item: menu::Item) -> Self {
self.items.alter_typed(TypedOp::Add(Typed::with(item)));
self
}
/// Modifica la lista de ítems (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<menu::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Menu GETTERS >***************************************************************************
/// Devuelve las clases CSS asociadas al menú.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve la lista de ítems (`children`) del menú.
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,73 @@
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Submenu {
id : AttrId,
title: AttrL10n,
items: Children,
}
impl Component for Submenu {
fn new() -> Self {
Submenu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="menu__submenu" {
@if let Some(title) = self.title().lookup(cx) {
h4 class="menu__submenu-title" { (title) }
}
ul {
(self.items().render(cx))
}
}
})
}
}
impl Submenu {
// **< Submenu BUILDER >************************************************************************
/// Establece el identificador único (`id`) del submenú.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title.alter_value(title);
self
}
/// Añade un nuevo ítem al submenú.
pub fn add_item(mut self, item: menu::Item) -> Self {
self.items.alter_typed(TypedOp::Add(Typed::with(item)));
self
}
/// Modifica la lista de ítems (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<menu::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Submenu GETTERS >************************************************************************
pub fn title(&self) -> &AttrL10n {
&self.title
}
/// Devuelve la lista de ítems (`children`) del submenú.
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -1,24 +0,0 @@
# menus.rs
sample_menus_item_label = Label
sample_menus_item_link = Link
sample_menus_item_blank = External link
sample_menus_item_disabled = Disabled link
sample_menus_test_title = Dropdown
sample_menus_dev_header = Intro
sample_menus_dev_getting_started = Getting started
sample_menus_dev_guides = Development guides
sample_menus_dev_forum = Developers forum
sample_menus_sdk_header = Software Development Kits
sample_menus_sdk_rust = SDKs Rust
sample_menus_sdk_js = SDKs JavaScript
sample_menus_sdk_python = SDKs Python
sample_menus_plugin_header = Plugins
sample_menus_plugin_auth = Rust Plugin Auth
sample_menus_plugin_cache = Rust Plugin Cache
sample_menus_item_sign_up = Sign up
sample_menus_item_login = Login

View file

@ -1,24 +0,0 @@
# menus.rs
sample_menus_item_label = Etiqueta
sample_menus_item_link = Enlace
sample_menus_item_blank = Enlace externo
sample_menus_item_disabled = Enlace deshabilitado
sample_menus_test_title = Desplegable
sample_menus_dev_header = Introducción
sample_menus_dev_getting_started = Primeros pasos
sample_menus_dev_guides = Guías de desarrollo
sample_menus_dev_forum = Foro de desarrolladores
sample_menus_sdk_header = Kits de Desarrollo Software
sample_menus_sdk_rust = SDKs de Rust
sample_menus_sdk_js = SDKs de JavaScript
sample_menus_sdk_python = SDKs de Python
sample_menus_plugin_header = Plugins
sample_menus_plugin_auth = Plugin Rust de autenticación
sample_menus_plugin_cache = Plugin Rust de caché
sample_menus_item_sign_up = Registrarse
sample_menus_item_login = Iniciar sesión

384
static/css/menu.css Normal file
View file

@ -0,0 +1,384 @@
/* Aislamiento & normalización */
.menu {
isolation: isolate;
}
@supports (all: revert) {
.menu {
all: revert;
display: block; }
}
.menu {
box-sizing: border-box;
line-height: var(--val-menu--line-height, 1.5);
color: var(--val-color--text);
text-align: left;
text-transform: none;
letter-spacing: normal;
word-spacing: normal;
white-space: normal;
cursor: default;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
height: auto;
margin: 0;
padding: 0;
z-index: 9999;
border: 0;
background: var(--val-menu--color-bg);
}
.menu *,
.menu *::before,
.menu *::after {
box-sizing: inherit;
}
.menu :where(a, button) {
appearance: none;
background: none;
border: 0;
font: inherit;
color: inherit;
text-decoration: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.menu :where(a, button):focus-visible {
outline: 2px solid var(--val-menu--color-highlight);
outline-offset: 2px;
}
.menu :where(ul, ol) {
list-style: none;
margin: 0;
padding: 0;
}
.menu svg {
fill: currentColor;
}
/* Estructura */
.menu__wrapper {
padding-right: var(--val-gap);
}
.menu__nav li {
display: inline-block;
margin: 0;
margin-inline-start: 1.5rem;
padding: 0;
line-height: var(--val-menu--item-height);
list-style: none;
}
.menu__item--label,
.menu__nav li > .menu__link {
position: relative;
font-weight: normal;
text-rendering: optimizeLegibility;
font-size: 1.45rem;
}
.menu__nav li > .menu__link {
transition: color 0.3s ease-in-out;
}
.menu__nav li:hover > .menu__link,
.menu__nav li > .menu__link:focus {
color: var(--val-menu--color-highlight);
}
.menu__nav li > .menu__link > svg.icon {
margin-left: 0.25rem;
}
.menu__children {
position: absolute;
max-width: 100%;
height: auto;
padding: var(--val-gap-0-5) var(--val-gap-1-5);
border: 0;
background: var(--val-menu--color-bg);
border-top: 3px solid var(--val-menu--color-highlight);
z-index: 500;
opacity: 0;
visibility: hidden;
box-shadow: 0 4px 6px -1px var(--val-menu--color-border), 0 2px 4px -1px var(--val-menu--color-shadow);
transition: all 0.3s ease-in-out;
}
.menu__item--children:hover > .menu__children,
.menu__item--children > .menu__link:focus + .menu__children,
.menu__item--children .menu__children:focus-within {
margin-top: 0.4rem;
opacity: 1;
visibility: visible;
}
.menu__submenu {
min-width: var(--val-menu--item-width-min);
max-width: var(--val-menu--item-width-max);
}
.menu__submenu-title {
font-size: 1rem;
font-weight: normal;
margin: 0;
padding: var(--val-menu--line-padding) 0;
line-height: var(--val-menu--line-height);
border: 0;
color: var(--val-menu--color-highlight);
text-transform: uppercase;
text-rendering: optimizeLegibility;
}
.menu__submenu li {
display: block;
margin: 0;
}
.menu__children--mega {
left: 50%;
transform: translateX(-50%);
}
.menu__mega {
display: flex;
flex-wrap: nowrap;
}
.menu__header,
.menu__trigger {
display: none;
}
/* Responsive <= 62rem (992px) */
@media (max-width: 62rem) {
.menu__wrapper {
padding-right: var(--val-gap-0-5);
}
.menu__trigger {
width: var(--val-menu--trigger-width);
height: var(--val-menu--item-height);
display: flex;
flex-direction: column;
justify-content: center;
}
.menu__trigger svg.icon {
width: 2rem;
height: 2rem;
}
.menu__nav,
.menu__children {
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.menu__nav {
position: fixed;
top: 0;
left: 0;
width: var(--val-menu--side-width);
height: 100%;
z-index: 9099;
overflow: hidden;
background: var(--val-menu--color-bg);
transform: translate(-100%);
transition: transform .5s ease-in-out, opacity .5s ease-in-out;
will-change: transform;
backface-visibility: hidden;
visibility: hidden;
pointer-events: none;
}
.menu__nav.active {
transform: translate(0%);
visibility: visible;
pointer-events: auto;
}
.menu__nav li {
display: block;
margin: 0;
line-height: var(--val-menu--line-height);
}
.menu__item--label,
.menu__nav li > .menu__link {
display: block;
text-align: inherit;
width: 100%;
padding: var(--val-menu--line-padding) var(--val-menu--item-height) var(--val-menu--line-padding) var(--val-menu--item-gap);
border-bottom: 1px solid var(--val-menu--color-border);
}
.menu__nav li ul li.menu__item--label,
.menu__nav li ul li > .menu__link {
border-bottom: 0;
}
.menu__nav li > .menu__link > svg.icon {
position: absolute;
top: var(--val-menu--line-padding);
right: var(--val-menu--line-padding);
height: var(--val-menu--line-height);
font-size: 1.25rem;
transform: rotate(-90deg);
}
.menu__children {
position: absolute;
display: none;
top: 0;
left: 0;
max-width: none;
min-width: auto;
width: 100%;
height: 100%;
margin: 0 !important;
padding: 0;
border-top: 0;
opacity: 1;
overflow-y: auto;
visibility: visible;
transform: translateX(0%);
box-shadow: none;
transition: opacity .5s ease-in-out, transform .5s ease-in-out, margin-top .5s ease-in-out;
}
.menu__children.active {
display: block;
}
.menu__children > :first-child {
margin-top: 2.675rem;
}
.menu__submenu-title {
padding: var(--val-menu--line-padding) var(--val-menu--item-height) var(--val-menu--line-padding) var(--val-menu--item-gap);
}
.menu__mega {
display: block;
}
.menu__header {
position: sticky;
display: flex;
align-items: center;
justify-content: space-between;
top: 0;
height: var(--val-menu--item-height);
border-bottom: 1px solid var(--val-menu--color-border);
background: var(--val-menu--color-bg);
z-index: 501;
}
.menu__title {
padding: var(--val-menu--line-padding);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.45rem;
font-weight: normal;
opacity: 0;
transform: translateY(.25rem);
transition: opacity .5s ease-in-out, transform .5s ease-in-out;
will-change: opacity, transform;
}
.menu__header.active .menu__title {
opacity: 1;
transform: translateY(0);
}
.menu__close,
.menu__back {
width: var(--val-menu--item-height);
min-width: var(--val-menu--item-height);
height: var(--val-menu--item-height);
line-height: var(--val-menu--item-height);
color: var(--val-color--text);
display: flex;
align-items: center;
justify-content: center;
background: var(--val-menu--color-bg);
}
.menu__close {
font-size: 2.25rem;
border: 1px solid var(--val-menu--color-border) !important;
border-width: 0 0 1px 1px !important;
}
.menu__back {
font-size: 1.25rem;
border: 1px solid var(--val-menu--color-border) !important;
border-width: 0 1px 1px 0 !important;
display: none;
}
.menu__header.active .menu__back {
display: flex;
}
.menu__list {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 0;
margin: 0;
}
.menu__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9098;
opacity: 0;
visibility: hidden;
background: rgba(0, 0, 0, 0.55);
transition: opacity .5s ease-in-out, visibility 0s linear .5s;
}
.menu__overlay.active {
opacity: 1;
visibility: visible;
transition-delay: 0s, 0s;
}
}
@media (hover: hover) and (pointer: fine) {
.menu__item--children:hover > .menu__children {
margin-top: 0.4rem;
opacity: 1;
visibility: visible;
}
.menu.menu--closing .menu__children {
margin-top: 0 !important;
opacity: 0 !important;
visibility: hidden !important;
}
}
@media (prefers-reduced-motion: reduce) {
.menu__nav,
.menu__children,
.menu__title,
.menu__overlay {
transition: none !important;
animation: none !important;
}
}
/* Animaciones */
@keyframes slideLeft {
0% {
opacity: 0;
transform: translateX(100%);
}
100% {
opacity: 1;
transform: translateX(0%);
}
}
@keyframes slideRight {
0% {
opacity: 1;
transform: translateX(0%);
}
100% {
opacity: 0;
transform: translateX(100%);
}
}

95
static/js/menu.js Normal file
View file

@ -0,0 +1,95 @@
const getTitle = (li) => li.querySelector('.menu__label')?.textContent.trim() ?? '';
function menu__showChildren(nav, children) {
const li = children[0];
const submenu = li.querySelector('.menu__children');
submenu.classList.add('active');
submenu.style.animation = 'slideLeft 0.5s ease forwards';
nav.querySelector('.menu__title').textContent = getTitle(li);;
nav.querySelector('.menu__header').classList.add('active');
}
function menu__hideChildren(nav, children) {
const submenu = children[0].querySelector('.menu__children');
submenu.style.animation = 'slideRight 0.5s ease forwards';
setTimeout(() => {
submenu.classList.remove('active');
submenu.style.removeProperty('animation');
}, 300);
children.shift();
if (children.length > 0) {
nav.querySelector('.menu__title').textContent = getTitle(children[0]);
} else {
nav.querySelector('.menu__header').classList.remove('active');
nav.querySelector('.menu__title').textContent = '';
}
}
function menu__toggle(nav, overlay) {
nav.classList.toggle('active');
overlay.classList.toggle('active');
}
function menu__reset(menu, nav, overlay) {
menu__toggle(nav, overlay);
setTimeout(() => {
nav.querySelector('.menu__header').classList.remove('active');
nav.querySelector('.menu__title').textContent = '';
menu.querySelectorAll('.menu__children').forEach(submenu => {
submenu.classList.remove('active');
submenu.style.removeProperty('animation');
});
}, 300);
return [];
}
document.querySelectorAll('.menu').forEach(menu => {
let menuChildren = [];
const menuNav = menu.querySelector('.menu__nav');
const menuOverlay = menu.querySelector('.menu__overlay');
menu.querySelector('.menu__list').addEventListener('click', (e) => {
if (menuNav.classList.contains('active')) {
let target = e.target.closest('.menu__item--children');
if (target && target != menuChildren[0]) {
menuChildren.unshift(target);
menu__showChildren(menuNav, menuChildren);
}
}
});
menu.querySelector('.menu__back').addEventListener('click', () => {
menu__hideChildren(menuNav, menuChildren);
});
menu.querySelector('.menu__close').addEventListener('click', () => {
menuChildren = menu__reset(menu, menuNav, menuOverlay);
});
menu.querySelectorAll('.menu__item--link > a[target="_blank"]').forEach(link => {
link.addEventListener('click', (e) => {
menuChildren = menu__reset(menu, menuNav, menuOverlay);
e.target.blur();
});
});
menu.querySelector('.menu__trigger').addEventListener('click', () => {
menu__toggle(menuNav, menuOverlay);
});
menuOverlay.addEventListener('click', () => {
menu__toggle(menuNav, menuOverlay);
});
let resizeTimeout;
window.addEventListener('resize', () => {
if (menuNav.classList.contains('active')) {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
menuChildren = menu__reset(menu, menuNav, menuOverlay);
}, 150);
}
});
});