diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs deleted file mode 100644 index 22ef336..0000000 --- a/examples/navbar-menus.rs +++ /dev/null @@ -1,101 +0,0 @@ -use pagetop::prelude::*; - -use pagetop_bootsier::prelude::*; - -struct SuperMenu; - -impl Extension for SuperMenu { - fn dependencies(&self) -> Vec { - 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 -} diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas.rs b/extensions/pagetop-bootsier/src/theme/offcanvas.rs index 18cc253..dc90534 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas.rs @@ -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")) diff --git a/src/base/component.rs b/src/base/component.rs index 508a28e..8991d72 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -62,3 +62,5 @@ pub use poweredby::PoweredBy; mod icon; pub use icon::{Icon, IconKind}; + +pub mod menu; diff --git a/src/base/component/menu.rs b/src/base/component/menu.rs new file mode 100644 index 0000000..14f7589 --- /dev/null +++ b/src/base/component/menu.rs @@ -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}; diff --git a/src/base/component/menu/element.rs b/src/base/component/menu/element.rs new file mode 100644 index 0000000..6d14204 --- /dev/null +++ b/src/base/component/menu/element.rs @@ -0,0 +1,56 @@ +use crate::prelude::*; + +type Content = Typed; +type SubmenuItems = Typed; + +#[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 + } +} diff --git a/src/base/component/menu/group.rs b/src/base/component/menu/group.rs new file mode 100644 index 0000000..ba18893 --- /dev/null +++ b/src/base/component/menu/group.rs @@ -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 { + 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) -> 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) -> Self { + self.elements.alter_typed(op); + self + } + + // **< Group GETTERS >************************************************************************** + + /// Devuelve la lista de elementos (`children`) del grupo. + pub fn elements(&self) -> &Children { + &self.elements + } +} diff --git a/src/base/component/menu/item.rs b/src/base/component/menu/item.rs new file mode 100644 index 0000000..c634218 --- /dev/null +++ b/src/base/component/menu/item.rs @@ -0,0 +1,185 @@ +use crate::prelude::*; + +type Label = L10n; +type Content = Typed; +type SubmenuItems = Typed; +type MegamenuGroups = Typed; + +#[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, + right_icon : Typed, +} + +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>(mut self, icon: Option) -> Self { + self.left_icon.alter_component(icon.map(Into::into)); + self + } + + #[builder_fn] + pub fn with_right_icon>(mut self, icon: Option) -> 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 { + &self.left_icon + } + + pub fn right_icon(&self) -> &Typed { + &self.right_icon + } +} diff --git a/src/base/component/menu/megamenu.rs b/src/base/component/menu/megamenu.rs new file mode 100644 index 0000000..e435e75 --- /dev/null +++ b/src/base/component/menu/megamenu.rs @@ -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 { + 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) -> 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) -> Self { + self.groups.alter_typed(op); + self + } + + // **< Megamenu GETTERS >*********************************************************************** + + /// Devuelve la lista de grupos (`children`) del megamenú. + pub fn groups(&self) -> &Children { + &self.groups + } +} diff --git a/src/base/component/menu/menu_menu.rs b/src/base/component/menu/menu_menu.rs new file mode 100644 index 0000000..58a4c21 --- /dev/null +++ b/src/base/component/menu/menu_menu.rs @@ -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 { + 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::(PARAM_BASE_INCLUDE_MENU_ASSETS, &true); + // cx.set_param::(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) -> 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) -> 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) -> 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 + } +} diff --git a/src/base/component/menu/submenu.rs b/src/base/component/menu/submenu.rs new file mode 100644 index 0000000..a5957ef --- /dev/null +++ b/src/base/component/menu/submenu.rs @@ -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 { + 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) -> 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) -> 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 + } +} diff --git a/src/locale/en-US/sample.ftl b/src/locale/en-US/sample.ftl deleted file mode 100644 index ae5b9a7..0000000 --- a/src/locale/en-US/sample.ftl +++ /dev/null @@ -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 diff --git a/src/locale/es-ES/sample.ftl b/src/locale/es-ES/sample.ftl deleted file mode 100644 index 6578597..0000000 --- a/src/locale/es-ES/sample.ftl +++ /dev/null @@ -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 diff --git a/static/css/menu.css b/static/css/menu.css new file mode 100644 index 0000000..428ba15 --- /dev/null +++ b/static/css/menu.css @@ -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%); + } +} diff --git a/static/js/menu.js b/static/js/menu.js new file mode 100644 index 0000000..dca8e4d --- /dev/null +++ b/static/js/menu.js @@ -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); + } + }); +});