diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 5af5f9c..709ce57 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -266,10 +266,13 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { v }; - // Extrae atributos descartando la documentación para incluir en `alter_...()`. - let non_doc_attrs: Vec<_> = attrs + // Filtra los atributos descartando `#[doc]` y `#[inline]` para el método `alter_...()`. + let non_doc_or_inline_attrs: Vec<_> = attrs .iter() - .filter(|&a| !a.path().is_ident("doc")) + .filter(|a| { + let p = a.path(); + !p.is_ident("doc") && !p.is_ident("inline") + }) .cloned() .collect(); @@ -284,14 +287,21 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { #(#attrs)* fn #with_name #generics (self, #(#args),*) -> Self #where_clause; - #(#non_doc_attrs)* + #(#non_doc_or_inline_attrs)* #[doc = #alter_doc] fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; } } Some(body) => { + // Si no se indicó ninguna forma de `inline`, fuerza `#[inline]` para `with_...()`. + let force_inline = if attrs.iter().any(|a| a.path().is_ident("inline")) { + quote! {} + } else { + quote! { #[inline] } + }; let with_fn = if is_trait { quote! { + #force_inline #vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause { let mut s = self; s.#alter_ident(#(#call_idents),*); @@ -300,6 +310,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { } } else { quote! { + #force_inline #vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause { self.#alter_ident(#(#call_idents),*); self @@ -310,7 +321,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { #(#attrs)* #with_fn - #(#non_doc_attrs)* + #(#non_doc_or_inline_attrs)* #[doc = #alter_doc] #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { #body diff --git a/src/base/component/menu/item.rs b/src/base/component/menu/item.rs index 7a36fbb..c9f7903 100644 --- a/src/base/component/menu/item.rs +++ b/src/base/component/menu/item.rs @@ -49,7 +49,7 @@ impl Component for Item { }), ItemKind::Link(label, path) => PrepareMarkup::With(html! { li class="menu__item menu__item--link" { - a href=(path(cx)) title=[description] { + a class="menu__link" href=(path(cx)) title=[description] { (left_icon) span class="menu__label" { (label.using(cx)) } (right_icon) @@ -58,7 +58,7 @@ impl Component for Item { }), ItemKind::LinkBlank(label, path) => PrepareMarkup::With(html! { li class="menu__item menu__item--link" { - a href=(path(cx)) title=[description] target="_blank" { + a class="menu__link" href=(path(cx)) title=[description] target="_blank" { (left_icon) span class="menu__label" { (label.using(cx)) } (right_icon) @@ -72,7 +72,7 @@ impl Component for Item { }), ItemKind::Submenu(label, submenu) => PrepareMarkup::With(html! { li class="menu__item menu__item--children" { - a href="#" title=[description] { + button type="button" class="menu__link" title=[description] { (left_icon) span class="menu__label" { (label.using(cx)) } (Icon::svg(html! { @@ -86,7 +86,7 @@ impl Component for Item { }), ItemKind::Megamenu(label, megamenu) => PrepareMarkup::With(html! { li class="menu__item menu__item--children" { - a href="#" title=[description] { + button type="button" class="menu__link" title=[description] { (left_icon) span class="menu__label" { (label.using(cx)) } (Icon::svg(html! { diff --git a/src/base/theme.rs b/src/base/theme.rs index 40129bf..a4b2df5 100644 --- a/src/base/theme.rs +++ b/src/base/theme.rs @@ -1,4 +1,4 @@ //! Temas básicos soportados por PageTop. mod basic; -pub use basic::Basic; +pub use basic::{Basic, BasicRegion}; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index ecd6485..b6a982f 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -1,6 +1,9 @@ /// Es el tema básico que incluye PageTop por defecto. use crate::prelude::*; +/// El tema básico usa las mismas regiones predefinidas por [`ThemeRegion`]. +pub type BasicRegion = ThemeRegion; + /// Tema básico por defecto. /// /// Ofrece las siguientes composiciones (*layouts*): @@ -46,12 +49,8 @@ impl Theme for Basic { } fn after_render_page_body(&self, page: &mut Page) { - let styles = match page.layout() { - "Intro" => "/css/intro.css", - "PageTopIntro" => "/css/intro.css", - _ => "/css/basic.css", - }; let pkg_version = env!("CARGO_PKG_VERSION"); + page.alter_assets(ContextOp::AddStyleSheet( StyleSheet::from("/css/normalize.css") .with_version("8.0.1") @@ -62,6 +61,11 @@ impl Theme for Basic { .with_version(pkg_version) .with_weight(-99), )) + .alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/css/basic.css") + .with_version(pkg_version) + .with_weight(-99), + )) .alter_assets(ContextOp::AddStyleSheet( StyleSheet::from("/css/components.css") .with_version(pkg_version) @@ -72,11 +76,6 @@ impl Theme for Basic { .with_version(pkg_version) .with_weight(-99), )) - .alter_assets(ContextOp::AddStyleSheet( - StyleSheet::from(styles) - .with_version(pkg_version) - .with_weight(-99), - )) .alter_assets(ContextOp::AddJavaScript( JavaScript::defer("/js/menu.js") .with_version(pkg_version) @@ -86,42 +85,52 @@ impl Theme for Basic { } fn render_intro(page: &mut Page) -> Markup { + page.alter_assets(ContextOp::AddStyleSheet( + StyleSheet::from("/css/intro.css").with_version(env!("CARGO_PKG_VERSION")), + )); + let title = page.title().unwrap_or_default(); let intro = page.description().unwrap_or_default(); + let theme = page.context().theme(); + let h = theme.render_page_region(page, &BasicRegion::Header); + let c = theme.render_page_region(page, &BasicRegion::Content); + let f = theme.render_page_region(page, &BasicRegion::Footer); + let intro_button_txt: L10n = page.param_or_default("intro_button_txt"); let intro_button_lnk: Option<&String> = page.param("intro_button_lnk"); html! { - body id=[page.body_id().get()] class=[page.body_classes().get()] { - header class="intro-header" { - section class="intro-header__body" { - h1 class="intro-header__title" { - span { (title) } - (intro) - } + header class="intro-header" { + section class="intro-header__body" { + h1 class="intro-header__title" { + span { (title) } + (intro) } - aside class="intro-header__image" aria-hidden="true" { - div class="intro-header__monster" { - picture { - source - type="image/avif" - src="/img/monster-pagetop_250.avif" - srcset="/img/monster-pagetop_500.avif 1.5x"; - source - type="image/webp" - src="/img/monster-pagetop_250.webp" - srcset="/img/monster-pagetop_500.webp 1.5x"; - img - src="/img/monster-pagetop_250.png" - srcset="/img/monster-pagetop_500.png 1.5x" - alt="Monster PageTop"; - } + } + aside class="intro-header__image" aria-hidden="true" { + div class="intro-header__monster" { + picture { + source + type="image/avif" + src="/img/monster-pagetop_250.avif" + srcset="/img/monster-pagetop_500.avif 1.5x"; + source + type="image/webp" + src="/img/monster-pagetop_250.webp" + srcset="/img/monster-pagetop_500.webp 1.5x"; + img + src="/img/monster-pagetop_250.png" + srcset="/img/monster-pagetop_500.png 1.5x" + alt="Monster PageTop"; } } } - main class="intro-content" { - section class="intro-content__body" { + (h) + } + main class="intro-content" { + section class="intro-content__body" { + div class="intro-text" { @if intro_button_lnk.is_some() { div class="intro-button" { a @@ -137,33 +146,34 @@ fn render_intro(page: &mut Page) -> Markup { } } } - div class="intro-text" { (page.render_region("content")) } + (c) } } - footer class="intro-footer" { - section class="intro-footer__body" { - div class="intro-footer__logo" { - svg - viewBox="0 0 1614 1614" - xmlns="http://www.w3.org/2000/svg" - role="img" - aria-label=[L10n::l("pagetop_logo").lookup(page)] - preserveAspectRatio="xMidYMid slice" - focusable="false" - { - path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {} - path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {} - path fill="rgb(255,255,255)" d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {} - } - } - div class="intro-footer__links" { - a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") } - a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") } - a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("intro_code").using(page)) } - em { (L10n::l("intro_have_fun").using(page)) } + } + footer class="intro-footer" { + section class="intro-footer__body" { + div class="intro-footer__logo" { + svg + viewBox="0 0 1614 1614" + xmlns="http://www.w3.org/2000/svg" + role="img" + aria-label=[L10n::l("pagetop_logo").lookup(page)] + preserveAspectRatio="xMidYMid slice" + focusable="false" + { + path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {} + path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {} + path fill="rgb(255,255,255)" d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {} } } + div class="intro-footer__links" { + a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") } + a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") } + a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("intro_code").using(page)) } + em { (L10n::l("intro_have_fun").using(page)) } + } } + (f) } } } diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 9dad7f5..f58d381 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,5 +1,6 @@ +use crate::core::component::ChildOp; use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; -use crate::core::theme::ThemeRef; +use crate::core::theme::{ChildrenInRegions, ThemeRef}; use crate::core::TypeInfo; use crate::html::{html, Markup}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; @@ -104,6 +105,10 @@ pub trait Contextual: LangId { #[builder_fn] fn with_assets(self, op: ContextOp) -> Self; + /// Opera con [`ChildOp`] en una región (`region_key`) de la página. + #[builder_fn] + fn with_child_in(self, region_key: &'static str, op: ChildOp) -> Self; + // **< Contextual GETTERS >********************************************************************* /// Devuelve una referencia a la solicitud HTTP asociada, si existe. @@ -211,6 +216,7 @@ pub struct Context { favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. + regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. id_counter : usize, // Contador para generar identificadores únicos. } @@ -250,6 +256,7 @@ impl Context { favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), + regions : ChildrenInRegions::default(), params : HashMap::default(), id_counter : 0, } @@ -283,6 +290,13 @@ impl Context { markup } + /// Renderiza los componentes de una región (`region_key`). + pub fn render_components_of(&mut self, region_key: &'static str) -> Markup { + self.regions + .merge_all_components(self.theme, region_key) + .render(self) + } + // **< Context PARAMS >************************************************************************* /// Recupera una *referencia tipada* al parámetro solicitado. @@ -471,6 +485,12 @@ impl Contextual for Context { self } + #[builder_fn] + fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + self.regions.alter_child_in(region_key, op); + self + } + // **< Contextual GETTERS >********************************************************************* fn request(&self) -> Option<&HttpRequest> { diff --git a/src/core/theme.rs b/src/core/theme.rs index 61d820b..64f40f3 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -15,10 +15,10 @@ //! [`Theme`]. mod definition; -pub use definition::{Theme, ThemePage, ThemeRef}; +pub use definition::{Theme, ThemePage, ThemeRef, ThemeRegion}; mod regions; pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; -pub use regions::{InRegion, Region}; +pub use regions::{InRegion, Region, RegionRef}; pub(crate) mod all; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 38a0bfc..7ef95c4 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -1,9 +1,9 @@ use crate::core::extension::Extension; -use crate::core::theme::Region; -use crate::global; +use crate::core::theme::{Region, RegionRef, REGION_CONTENT}; use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; +use crate::{global, join}; use std::sync::LazyLock; @@ -13,78 +13,138 @@ use std::sync::LazyLock; /// implementen [`Theme`] y, a su vez, [`Extension`]. pub type ThemeRef = &'static dyn Theme; +/// Conjunto de regiones que los temas pueden exponer para el renderizado. +/// +/// `ThemeRegion` define un conjunto de regiones predefinidas para estructurar un documento HTML. +/// Proporciona **identificadores estables** (vía [`Region::key()`]) y **etiquetas localizables** +/// (vía [`Region::label()`]) a las regiones donde se añadirán los componentes. +/// +/// Se usa por defecto en [`Theme::page_regions()`](crate::core::theme::Theme::page_regions) y sus +/// variantes representan el conjunto mínimo recomendado para cualquier tema. Sin embargo, cada tema +/// podría exponer su propio conjunto de regiones. +pub enum ThemeRegion { + /// Cabecera de la página. + /// + /// Clave: `"header"`. Suele contener *branding*, navegación principal o avisos globales. + Header, + + /// Contenido principal de la página (**obligatoria**). + /// + /// Clave: `"content"`. Es el destino por defecto para insertar componentes a nivel de página. + Content, + + /// Pie de página. + /// + /// Clave: `"footer"`. Suele contener enlaces legales, créditos o navegación secundaria. + Footer, +} + +impl Region for ThemeRegion { + fn key(&self) -> &str { + match self { + ThemeRegion::Header => "header", + ThemeRegion::Content => REGION_CONTENT, + ThemeRegion::Footer => "footer", + } + } + + fn label(&self) -> L10n { + L10n::l(join!("region_", self.key())) + } +} + /// Métodos predefinidos de renderizado para las páginas de un tema. /// -/// Contiene las implementaciones base de las **secciones** `` y ``. Se implementa -/// automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente no requiere -/// implementación explícita. +/// Contiene las implementaciones base para renderizar las **secciones** `` y ``. Se +/// implementa automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente +/// no requiere implementación explícita. /// -/// Si un tema **sobrescribe** [`render_page_head()`](Theme::render_page_head) o -/// [`render_page_body()`](Theme::render_page_body), se puede volver al comportamiento por defecto -/// cuando se necesite usando FQS (*Fully Qualified Syntax*): +/// Si un tema **sobrescribe** uno o más de estos métodos de [`Theme`]: +/// +/// - [`render_page_region()`](Theme::render_page_region), +/// - [`render_page_head()`](Theme::render_page_head), o +/// - [`render_page_body()`](Theme::render_page_body); +/// +/// es posible volver al comportamiento por defecto usando FQS (*Fully Qualified Syntax*): /// /// - `::render_body(self, page, self.page_regions())` /// - `::render_head(self, page)` pub trait ThemePage { - /// Renderiza el contenido del `` de la página. + /// Renderiza el **contenedor** de una región concreta del `` de la página. /// - /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, genera un - /// contenedor con `role="region"` y un `aria-label` localizado. Se asume que cada identificador - /// de región es **único** dentro de la página. - fn render_body(&self, page: &mut Page, regions: &[(Region, L10n)]) -> Markup { + /// Obtiene los componentes asociados a `region.key()` desde el contexto de la página y, si hay + /// salida, envuelve el contenido en un contenedor `
` predefinido. + /// + /// Si la región **no produce contenido**, devuelve un `Markup` vacío. + #[inline] + fn render_region(&self, page: &mut Page, region: RegionRef) -> Markup { html! { - body id=[page.body_id().get()] class=[page.body_classes().get()] { - @for (region, region_label) in regions { - @let output = page.render_region(region.key()); - @if !output.is_empty() { - @let region_name = region.name(); - div - id=(region_name) - class={ "region region--" (region_name) } - role="region" - aria-label=[region_label.lookup(page)] - { - (output) - } - } + @let key = region.key(); + @let output = page.context().render_components_of(key); + @if !output.is_empty() { + div + id=(key) + class={ "region region--" (key) } + role="region" + aria-label=[region.label().lookup(page)] + { + (output) } } } } - /// Renderiza el contenido del `` de la página. + /// Renderiza el **contenido interior** del `` de la página. /// - /// Por defecto incluye las etiquetas básicas (`charset`, `title`, `description`, `viewport`, + /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, delega en + /// [`render_region()`](Self::render_region) la generación del contenedor. Las regiones sin + /// contenido **no** producen salida. Se asume que cada identificador de región es **único** + /// dentro de la página. + /// + /// La etiqueta `` no se incluye aquí; únicamente renderiza su contenido. + #[inline] + fn render_body(&self, page: &mut Page, regions: &[RegionRef]) -> Markup { + html! { + @for region in regions { + (self.render_region(page, *region)) + } + } + } + + /// Renderiza el **contenido interior** del `` de la página. + /// + /// Incluye por defecto las etiquetas básicas (`charset`, `title`, `description`, `viewport`, /// `X-UA-Compatible`), los metadatos (`name/content`) y propiedades (`property/content`), /// además de los recursos CSS/JS de la página. + /// + /// La etiqueta `` no se incluye aquí; únicamente se renderiza su contenido. + #[inline] fn render_head(&self, page: &mut Page) -> Markup { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { - head { - meta charset="utf-8"; + meta charset="utf-8"; - @if let Some(title) = page.title() { - title { (global::SETTINGS.app.name) (" | ") (title) } - } @else { - title { (global::SETTINGS.app.name) } - } - - @if let Some(description) = page.description() { - meta name="description" content=(description); - } - - meta name="viewport" content=(viewport); - @for (name, content) in page.metadata() { - meta name=(name) content=(content) {} - } - - meta http-equiv="X-UA-Compatible" content="IE=edge"; - @for (property, content) in page.properties() { - meta property=(property) content=(content) {} - } - - (page.render_assets()) + @if let Some(title) = page.title() { + title { (global::SETTINGS.app.name) (" | ") (title) } + } @else { + title { (global::SETTINGS.app.name) } } + + @if let Some(description) = page.description() { + meta name="description" content=(description); + } + + meta name="viewport" content=(viewport); + @for (name, content) in page.metadata() { + meta name=(name) content=(content) {} + } + + meta http-equiv="X-UA-Compatible" content="IE=edge"; + @for (property, content) in page.properties() { + meta property=(property) content=(content) {} + } + + (page.context().render_assets()) } } } @@ -125,31 +185,53 @@ pub trait Theme: Extension + ThemePage + Send + Sync { /// Declaración ordenada de las regiones disponibles en la página. /// - /// Devuelve una **lista estática** de pares `(Region, L10n)` que se usará para renderizar todas - /// las regiones que componen una página en el orden indicado . + /// Retorna una **lista estática** de referencias ([`RegionRef`](crate::core::theme::RegionRef)) + /// que representan las regiones que el tema admite dentro del ``. /// - /// Si un tema necesita un conjunto distinto de regiones, se puede **sobrescribir** este método - /// con los siguientes requisitos y recomendaciones: + /// Cada referencia apunta a una instancia que implementa [`Region`](crate::core::theme::Region) + /// para definir cada región de forma segura y estable. Y si un tema necesita un conjunto + /// distinto de regiones, puede **sobrescribir** este método siguiendo estas recomendaciones: /// - /// - Los identificadores deben ser **estables** (p. ej. `"sidebar-left"`, `"content"`). - /// - La región `"content"` es **obligatoria**. Se puede usar [`Region::default()`] para - /// declararla. - /// - La etiqueta `L10n` se evalúa con el idioma activo de la página. + /// - Los identificadores devueltos por [`Region::key()`](crate::core::theme::Region::key) + /// deben ser **estables** (p. ej. `"sidebar-left"`, `"content"`). + /// - La región `"content"` es **obligatoria**, ya que se usa como destino por defecto para + /// insertar componentes y renderizarlos. + /// - El orden de la lista podría tener relevancia como **orden de renderizado** dentro del + /// `` segun la implementación de [`render_page_body()`](Self::render_page_body). + /// - Las etiquetas (`L10n`) de cada región se evaluarán con el idioma activo de la página. /// - /// Por defecto devuelve: + /// # Ejemplo /// - /// - `"header"`: cabecera. - /// - `"content"`: contenido principal (**obligatoria**). - /// - `"footer"`: pie. - fn page_regions(&self) -> &'static [(Region, L10n)] { - static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { + /// ```rust,ignore + /// fn page_regions(&self) -> &'static [RegionRef] { + /// static REGIONS: LazyLock<[RegionRef; 4]> = LazyLock::new(|| { + /// [ + /// &ThemeRegion::Header, + /// &ThemeRegion::Content, + /// &ThemeRegion::Footer, + /// ] + /// }); + /// &*REGIONS + /// } + /// ``` + fn page_regions(&self) -> &'static [RegionRef] { + static REGIONS: LazyLock<[RegionRef; 3]> = LazyLock::new(|| { [ - (Region::declare("header"), L10n::l("region_header")), - (Region::default(), L10n::l("region_content")), - (Region::declare("footer"), L10n::l("region_footer")), + &ThemeRegion::Header, + &ThemeRegion::Content, + &ThemeRegion::Footer, ] }); - ®IONS[..] + &*REGIONS + } + + /// Renderiza una región de la página. + /// + /// Si se sobrescribe este método, se puede volver al comportamiento base con: + /// `::render_region(self, page, region)`. + #[inline] + fn render_page_region(&self, page: &mut Page, region: RegionRef) -> Markup { + ::render_region(self, page, region) } /// Acciones específicas del tema antes de renderizar el `` de la página. diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 8082aac..ecb5eb5 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,5 +1,6 @@ use crate::core::component::{Child, ChildOp, Children}; use crate::core::theme::ThemeRef; +use crate::locale::L10n; use crate::{builder_fn, AutoDefault, UniqueId}; use parking_lot::RwLock; @@ -18,88 +19,95 @@ static COMMON_REGIONS: LazyLock> = /// Nombre de la región de contenido por defecto (`"content"`). pub const REGION_CONTENT: &str = "content"; -/// Identificador de una región de página. +/// Define la interfaz mínima que describe una **región de renderizado** dentro de una página. /// -/// Incluye una **clave estática** ([`key()`](Self::key)) que identifica la región en el tema, y un -/// **nombre normalizado** ([`name()`](Self::name)) en minúsculas para su uso en atributos HTML -/// (p.ej., clases `region__{name}`). +/// Una *región* representa una zona del documento HTML (por ejemplo: `"header"`, `"content"` o +/// `"sidebar-left"`), en la que se pueden incluir y renderizar componentes dinámicamente. /// -/// Se utiliza para declarar las regiones que componen una página en un tema (ver -/// [`page_regions()`](crate::core::theme::Theme::page_regions)). -pub struct Region { - key: &'static str, - name: String, -} - -impl Default for Region { - #[inline] - fn default() -> Self { - Self { - key: REGION_CONTENT, - name: REGION_CONTENT.to_string(), - } - } -} - -impl Region { - /// Declara una región a partir de su clave estática. +/// Este `trait` abstrae los metadatos básicos de cada región, esencialmente: +/// +/// - su **clave interna** (`key()`), que la identifica de forma única dentro de la página, y +/// - su **etiqueta localizada** (`label()`), que se usa como texto accesible (por ejemplo en +/// `aria-label` o en descripciones semánticas del contenedor). +/// +/// Las implementaciones típicas son *enumeraciones estáticas* declaradas por cada tema (ver como +/// ejemplo [`ThemeRegion`](crate::core::theme::ThemeRegion)), de modo que las claves y etiquetas +/// permanecen inmutables y fácilmente referenciables. +/// +/// # Ejemplo +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// pub enum MyThemeRegion { +/// Header, +/// Content, +/// Footer, +/// } +/// +/// impl Region for MyThemeRegion { +/// fn key(&self) -> &str { +/// match self { +/// MyThemeRegion::Header => "header", +/// MyThemeRegion::Content => "content", +/// MyThemeRegion::Footer => "footer", +/// } +/// } +/// +/// fn label(&self) -> L10n { +/// L10n::l(join!("region__", self.key())) +/// } +/// } +/// ``` +pub trait Region: Send + Sync { + /// Devuelve la **clave interna** que identifica de forma única una región. /// - /// Genera además un nombre normalizado de la clave, eliminando espacios iniciales y finales, - /// convirtiendo a minúsculas y sustituyendo los espacios intermedios por guiones (`-`). + /// La clave se utiliza para asociar los componentes de la región con su contenedor HTML + /// correspondiente. Por convención, se emplean nombres en minúsculas y con guiones (`"header"`, + /// `"main"`, `"sidebar-right"`, etc.), y la región `"content"` es **obligatoria** en todos los + /// temas. + fn key(&self) -> &str; + + /// Devuelve la **etiqueta localizada** (`L10n`) asociada a la región. /// - /// Esta clave se usará para añadir componentes a la región; por ello se recomiendan nombres - /// sencillos, limitando los caracteres a `[a-z0-9-]` (p.ej., `"sidebar"` o `"main-menu"`), cuyo - /// nombre normalizado coincidirá con la clave. - #[inline] - pub fn declare(key: &'static str) -> Self { - Self { - key, - name: key.trim().to_ascii_lowercase().replace(' ', "-"), - } - } - - /// Devuelve la clave estática asignada a la región. - #[inline] - pub fn key(&self) -> &'static str { - self.key - } - - /// Devuelve el nombre normalizado de la región (para atributos y búsquedas). - #[inline] - pub fn name(&self) -> &str { - &self.name - } + /// Esta etiqueta se evalúa en el idioma activo de la página y se utiliza principalmente para + /// accesibilidad, como el valor de `aria-label` en el contenedor generado por + /// [`ThemePage::render_region()`](crate::core::theme::ThemePage::render_region). + fn label(&self) -> L10n; } +/// Referencia estática a una región. +pub type RegionRef = &'static dyn Region; + // Contenedor interno de componentes agrupados por región. #[derive(AutoDefault)] pub struct ChildrenInRegions(HashMap<&'static str, Children>); impl ChildrenInRegions { - pub fn with(region_name: &'static str, child: Child) -> Self { - ChildrenInRegions::default().with_child_in(region_name, ChildOp::Add(child)) + pub fn with(region_key: &'static str, child: Child) -> Self { + ChildrenInRegions::default().with_child_in(region_key, ChildOp::Add(child)) } #[builder_fn] - pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self { - if let Some(region) = self.0.get_mut(region_name) { + pub fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + if let Some(region) = self.0.get_mut(region_key) { region.alter_child(op); } else { - self.0.insert(region_name, Children::new().with_child(op)); + self.0.insert(region_key, Children::new().with_child(op)); } self } - pub fn merge_all_components(&self, theme_ref: ThemeRef, region_name: &'static str) -> Children { + pub fn merge_all_components(&self, theme_ref: ThemeRef, region_key: &'static str) -> Children { let common = COMMON_REGIONS.read(); if let Some(r) = THEME_REGIONS.read().get(&theme_ref.type_id()) { Children::merge(&[ - common.0.get(region_name), - self.0.get(region_name), - r.0.get(region_name), + common.0.get(region_key), + self.0.get(region_key), + r.0.get(region_key), ]) } else { - Children::merge(&[common.0.get(region_name), self.0.get(region_name)]) + Children::merge(&[common.0.get(region_key), self.0.get(region_key)]) } } } @@ -114,9 +122,9 @@ impl ChildrenInRegions { pub enum InRegion { /// Región de contenido por defecto. Content, - /// Región identificada por el nombre proporcionado. - Named(&'static str), - /// Región identificada por un nombre y asociada a un tema concreto. + /// Región identificada por la clave proporcionado. + Key(&'static str), + /// Región identificada por una clave para un tema concreto. OfTheme(&'static str, ThemeRef), } @@ -134,7 +142,7 @@ impl InRegion { /// ))); /// /// // Texto en la región "sidebar". - /// InRegion::Named("sidebar").add(Child::with(Html::with(|_| + /// InRegion::Key("sidebar").add(Child::with(Html::with(|_| /// html! { ("Publicidad") } /// ))); /// ``` @@ -145,19 +153,19 @@ impl InRegion { .write() .alter_child_in(REGION_CONTENT, ChildOp::Add(child)); } - InRegion::Named(region_name) => { + InRegion::Key(region_key) => { COMMON_REGIONS .write() - .alter_child_in(region_name, ChildOp::Add(child)); + .alter_child_in(region_key, ChildOp::Add(child)); } - InRegion::OfTheme(region_name, theme_ref) => { + InRegion::OfTheme(region_key, theme_ref) => { let mut regions = THEME_REGIONS.write(); if let Some(r) = regions.get_mut(&theme_ref.type_id()) { - r.alter_child_in(region_name, ChildOp::Add(child)); + r.alter_child_in(region_key, ChildOp::Add(child)); } else { regions.insert( theme_ref.type_id(), - ChildrenInRegions::with(region_name, child), + ChildrenInRegions::with(region_key, child), ); } } diff --git a/src/response/page.rs b/src/response/page.rs index f81c980..1649d54 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -5,7 +5,7 @@ pub use actix_web::Result as ResultPage; use crate::base::action; use crate::core::component::{Child, ChildOp, Component, Context, ContextOp, Contextual}; -use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; +use crate::core::theme::{ThemeRef, REGION_CONTENT}; use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{AttrClasses, ClassesOp}; @@ -29,7 +29,6 @@ pub struct Page { body_id : AttrId, body_classes: AttrClasses, context : Context, - regions : ChildrenInRegions, } impl Page { @@ -47,7 +46,6 @@ impl Page { body_id : AttrId::default(), body_classes: AttrClasses::default(), context : Context::new(Some(request)), - regions : ChildrenInRegions::default(), } } @@ -105,48 +103,37 @@ impl Page { /// **Obsoleto desde la versión 0.4.0**: usar [`add_component_in()`](Self::add_component_in) en /// su lugar. #[deprecated(since = "0.4.0", note = "Use `add_component_in()` instead")] - pub fn with_component_in(self, region_name: &'static str, component: impl Component) -> Self { - self.add_component_in(region_name, component) + pub fn with_component_in(self, region_key: &'static str, component: impl Component) -> Self { + self.add_component_in(region_key, component) } /// Añade un componente a la región de contenido por defecto. pub fn add_component(mut self, component: impl Component) -> Self { - self.regions + self.context .alter_child_in(REGION_CONTENT, ChildOp::Add(Child::with(component))); self } - /// Añade un componente en una región (`region_name`) de la página. - pub fn add_component_in( - mut self, - region_name: &'static str, - component: impl Component, - ) -> Self { - self.regions - .alter_child_in(region_name, ChildOp::Add(Child::with(component))); + /// Añade un componente en una región (`region_key`) de la página. + pub fn add_component_in(mut self, region_key: &'static str, component: impl Component) -> Self { + self.context + .alter_child_in(region_key, ChildOp::Add(Child::with(component))); self } /// **Obsoleto desde la versión 0.4.0**: usar [`with_child_in()`](Self::with_child_in) en su /// lugar. #[deprecated(since = "0.4.0", note = "Use `with_child_in()` instead")] - pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self { - self.alter_child_in(region_name, op); + pub fn with_child_in_region(mut self, region_key: &'static str, op: ChildOp) -> Self { + self.alter_child_in(region_key, op); self } /// **Obsoleto desde la versión 0.4.0**: usar [`alter_child_in()`](Self::alter_child_in) en su /// lugar. #[deprecated(since = "0.4.0", note = "Use `alter_child_in()` instead")] - pub fn alter_child_in_region(&mut self, region_name: &'static str, op: ChildOp) -> &mut Self { - self.alter_child_in(region_name, op); - self - } - - /// Opera con [`ChildOp`] en una región (`region_name`) de la página. - #[builder_fn] - pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self { - self.regions.alter_child_in(region_name, op); + pub fn alter_child_in_region(&mut self, region_key: &'static str, op: ChildOp) -> &mut Self { + self.alter_child_in(region_key, op); self } @@ -193,18 +180,6 @@ impl Page { // **< Page RENDER >**************************************************************************** - /// Renderiza los componentes de una región (`region_name`) de la página. - pub fn render_region(&mut self, region_name: &'static str) -> Markup { - self.regions - .merge_all_components(self.context.theme(), region_name) - .render(&mut self.context) - } - - /// Renderiza los recursos de la página. - pub fn render_assets(&mut self) -> Markup { - self.context.render_assets() - } - /// Renderiza la página completa en formato HTML. /// /// Ejecuta las acciones correspondientes antes y después de renderizar el ``, @@ -238,8 +213,14 @@ impl Page { Ok(html! { (DOCTYPE) html lang=(lang) dir=(dir) { - (head) - (body) + head { + (head) + } + body id=[self.body_id().get()] class=[self.body_classes().get()] { + (self.context.render_components_of("page-top")) + (body) + (self.context.render_components_of("page-bottom")) + } } }) } @@ -290,6 +271,12 @@ impl Contextual for Page { self } + #[builder_fn] + fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { + self.context.alter_child_in(region_key, op); + self + } + // **< Contextual GETTERS >********************************************************************* fn request(&self) -> Option<&HttpRequest> { diff --git a/static/css/basic.css b/static/css/basic.css index 04801dd..058e173 100644 --- a/static/css/basic.css +++ b/static/css/basic.css @@ -1,3 +1,19 @@ +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: var(--val-font-family); + font-size: var(--val-fs--base); + font-weight: var(--val-fw--base); + line-height: var(--val-lh--base); + color: var(--val-color--text); + background-color: var(--val-color--bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; +} + /* Page layout */ .region--footer { diff --git a/static/css/intro.css b/static/css/intro.css index 39c9d6a..774bbb2 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -1,37 +1,15 @@ -:root { - --bg-img: url('/img/intro-header.jpg'); - --bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--bg-img) type('image/jpeg')); - --bg-img-sm: url('/img/intro-header-sm.jpg'); - --bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--bg-img-sm) type('image/jpeg')); - --bg-color: #8c5919; - --color: #1a202c; - --color-gray: #e4e4e7; - --color-link: #1e4eae; - --color-block-1: #b689ff; - --color-block-2: #fecaca; - --color-block-3: #e6a9e2; - --color-block-4: #ffedca; - --color-block-5: #ffffff; - --focus-outline: 2px solid var(--color-link); - --focus-outline-offset: 2px; - --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); -} - html { min-height: 100%; background-color: black; } + body { margin: auto; position: relative; min-height: 100%; min-width: 350px; - background-color: var(--bg-color); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - font-size: 1.125rem; - font-weight: 300; - color: var(--color); - line-height: 1.6; + color: var(--intro-color); + background-color: var(--intro-bg-color); width: 100%; display: flex; @@ -51,12 +29,12 @@ a { transition: font-size 0.2s, text-decoration-color 0.2s; } a:focus-visible { - outline: var(--focus-outline); - outline-offset: var(--focus-outline-offset); + outline: var(--intro-focus-outline); + outline-offset: var(--intro-focus-outline-offset); } a:hover, a:hover:visited { - text-decoration-color: var(--color-link); + text-decoration-color: var(--intro-color-link); } /* @@ -69,9 +47,9 @@ a:hover:visited { width: 100%; max-width: 80rem; margin: 0 auto; - padding-bottom: 9rem; - background-image: var(--bg-img-sm); - background-image: var(--bg-img-sm-set); + padding-bottom: 4rem; + background-image: var(--intro-bg-img-sm); + background-image: var(--intro-bg-img-sm-set); background-position: top center; background-position-y: -1rem; background-size: contain; @@ -119,8 +97,8 @@ a:hover:visited { } @media (min-width: 64rem) { .intro-header { - background-image: var(--bg-img); - background-image: var(--bg-img-set); + background-image: var(--intro-bg-img); + background-image: var(--intro-bg-img-set); } .intro-header__title { padding: 1.2rem 2rem 2.6rem 2rem; @@ -180,8 +158,7 @@ a:hover:visited { .intro-button { width: 100%; - margin: 0 auto 3rem; - z-index: 10; + margin: 0 auto; } .intro-button__link { background: #7f1d1d; @@ -306,6 +283,9 @@ a:hover:visited { animation-play-state: paused; } @media (max-width: 48rem) { + .intro-header { + padding-bottom: 9rem;; + } .intro-button__link { height: 6.25rem; min-width: auto; @@ -342,17 +322,15 @@ a:hover:visited { font-size: 1.3125rem; font-weight: 400; line-height: 1.5; - margin-top: -6rem; margin-bottom: 0; background: #fff; position: relative; +} +.region--content { padding: 2.5rem 1.063rem 0.75rem; overflow: hidden; } -.intro-button + .intro-text { - padding-top: 6rem; -} -.intro-text p { +.region--content p { width: 100%; line-height: 150%; font-weight: 400; @@ -364,31 +342,39 @@ a:hover:visited { font-size: 1.375rem; line-height: 2rem; } - .intro-button + .intro-text { + .intro-button + .region--content { padding-top: 7rem; } } @media (min-width: 64rem) { - .intro-text { + .intro-header { + padding-bottom: 9rem;; + } + .intro-text, + .region--content { border-radius: 0.75rem; - box-shadow: var(--shadow); + } + .intro-text { + box-shadow: var(--intro-shadow); max-width: 60rem; margin: 0 auto 6rem; + } + .region--content { padding-left: 4.5rem; padding-right: 4.5rem; } } -.intro-text .block { +.region--content .block { position: relative; } -.intro-text .block__title { +.region--content .block__title { margin: 1em 0 .8em; } -.intro-text .block__title span { +.region--content .block__title span { display: inline-block; padding: 10px 30px 14px; - margin: 0 0 0 20px; + margin: 30px 0 0 20px; background: white; border: 5px solid; border-radius: 30px; @@ -396,7 +382,7 @@ a:hover:visited { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.intro-text .block__title:before { +.region--content .block__title:before { content: ""; height: 5px; position: absolute; @@ -409,7 +395,7 @@ a:hover:visited { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.intro-text .block__title:after { +.region--content .block__title:after { content: ""; height: 70rem; position: absolute; @@ -417,23 +403,23 @@ a:hover:visited { left: -15%; width: 130%; z-index: -10; - background: var(--color-block-1); + background: var(--intro-bg-block-1); transform: rotate(2deg); } -.intro-text .block:nth-of-type(5n+1) .block__title:after { - background: var(--color-block-1); +.region--content .block:nth-of-type(5n+1) .block__title:after { + background: var(--intro-bg-block-1); } -.intro-text .block:nth-of-type(5n+2) .block__title:after { - background: var(--color-block-2); +.region--content .block:nth-of-type(5n+2) .block__title:after { + background: var(--intro-bg-block-2); } -.intro-text .block:nth-of-type(5n+3) .block__title:after { - background: var(--color-block-3); +.region--content .block:nth-of-type(5n+3) .block__title:after { + background: var(--intro-bg-block-3); } -.intro-text .block:nth-of-type(5n+4) .block__title:after { - background: var(--color-block-4); +.region--content .block:nth-of-type(5n+4) .block__title:after { + background: var(--intro-bg-block-4); } -.intro-text .block:nth-of-type(5n+5) .block__title:after { - background: var(--color-block-5); +.region--content .block:nth-of-type(5n+5) .block__title:after { + background: var(--intro-bg-block-5); } /* @@ -443,7 +429,7 @@ a:hover:visited { .intro-footer { width: 100%; background-color: black; - color: var(--color-gray); + color: var(--intro-color-gray); padding-bottom: 2rem; } @@ -459,7 +445,7 @@ a:hover:visited { line-height: 100%; } .intro-footer__body a:visited { - color: var(--color-gray); + color: var(--intro-color-gray); } .intro-footer__logo, .intro-footer__links { @@ -492,5 +478,5 @@ a:hover:visited { /* PoweredBy component */ .poweredby a:visited { - color: var(--color-gray); + color: var(--intro-color-gray); } diff --git a/static/css/menu.css b/static/css/menu.css index 5520e39..6522f4a 100644 --- a/static/css/menu.css +++ b/static/css/menu.css @@ -1,54 +1,92 @@ +/* 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: none; - outline: none; + 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__wrapper a, -.menu__wrapper button { - cursor: pointer; - border: none; - background: none; - text-decoration: none; -} -.menu__nav ul { - margin: 0; - padding: 0; -} .menu__nav li { display: inline-block; - margin: 0 0 0 1.5rem; + margin: 0; + margin-inline-start: 1.5rem; padding: 0; line-height: var(--val-menu--item-height); list-style: none; - list-style-type: none; } .menu__item--label, -.menu__nav li > a { +.menu__nav li > .menu__link { position: relative; - font-weight: 500; - color: var(--val-color--text); + font-weight: normal; text-rendering: optimizeLegibility; + font-size: 1.45rem; } -.menu__nav li > a { - border: none; +.menu__nav li > .menu__link { transition: color 0.3s ease-in-out; } -.menu__nav li:hover > a, -.menu__nav li > a:focus { +.menu__nav li:hover > .menu__link, +.menu__nav li > .menu__link:focus { color: var(--val-menu--color-highlight); } -.menu__nav li > a > svg.icon { +.menu__nav li > .menu__link > svg.icon { margin-left: 0.25rem; } @@ -57,19 +95,18 @@ max-width: 100%; height: auto; padding: var(--val-gap-0-5) var(--val-gap-1-5); - border: none; - outline: none; + 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.5s ease-in-out; + transition: all 0.3s ease-in-out; } .menu__item--children:hover > .menu__children, -.menu__item--children > a:focus + .menu__children, +.menu__item--children > .menu__link:focus + .menu__children, .menu__item--children .menu__children:focus-within { margin-top: 0.4rem; opacity: 1; @@ -81,14 +118,12 @@ max-width: var(--val-menu--item-width-max); } .menu__submenu-title { - font-family: inherit; font-size: 1rem; - font-weight: 500; + font-weight: normal; margin: 0; padding: var(--val-menu--line-padding) 0; line-height: var(--val-menu--line-height); - border: none; - outline: none; + border: 0; color: var(--val-menu--color-highlight); text-transform: uppercase; text-rendering: optimizeLegibility; @@ -113,18 +148,15 @@ display: none; } -/* Applies <= 992px */ -@media only screen and (max-width: 62rem) { +/* Responsive <= 62rem (992px) */ + +@media (max-width: 62rem) { .menu__wrapper { padding-right: var(--val-gap-0-5); } .menu__trigger { - cursor: pointer; width: var(--val-menu--trigger-width); height: var(--val-menu--item-height); - border: none; - outline: none; - background: none; display: flex; flex-direction: column; justify-content: center; @@ -133,6 +165,13 @@ width: 2rem; height: 2rem; } + + .menu__nav, + .menu__children { + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + } + .menu__nav { position: fixed; top: 0; @@ -143,10 +182,16 @@ overflow: hidden; background: var(--val-menu--color-bg); transform: translate(-100%); - transition: all 0.5s ease-in-out; + 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 { @@ -156,16 +201,18 @@ } .menu__item--label, - .menu__nav li > a { + .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 > a { + .menu__nav li ul li > .menu__link { border-bottom: 0; } - .menu__nav li > a > svg.icon { + .menu__nav li > .menu__link > svg.icon { position: absolute; top: var(--val-menu--line-padding); right: var(--val-menu--line-padding); @@ -191,6 +238,7 @@ 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; @@ -223,6 +271,16 @@ 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 { @@ -231,18 +289,20 @@ height: var(--val-menu--item-height); line-height: var(--val-menu--item-height); color: var(--val-color--text); - cursor: pointer; display: flex; align-items: center; justify-content: center; + background: var(--val-menu--color-bg); } .menu__close { font-size: 2.25rem; - border-left: 1px solid var(--val-menu--color-border) !important; + border: 1px solid var(--val-menu--color-border) !important; + border-width: 0 0 1px 1px !important; } .menu__back { font-size: 1.25rem; - border-right: 1px solid var(--val-menu--color-border) !important; + border: 1px solid var(--val-menu--color-border) !important; + border-width: 0 1px 1px 0 !important; display: none; } .menu__header.active .menu__back { @@ -267,15 +327,34 @@ opacity: 0; visibility: hidden; background: rgba(0, 0, 0, 0.55); - transition: all 0.5s ease-in-out; + 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; } } -/* ANIMATIONS */ +@media (prefers-reduced-motion: reduce) { + .menu__nav, + .menu__children, + .menu__title, + .menu__overlay { + transition: none !important; + animation: none !important; + } +} + +/* Animaciones */ @keyframes slideLeft { 0% { diff --git a/static/css/root.css b/static/css/root.css index aeab1c6..270c1b3 100644 --- a/static/css/root.css +++ b/static/css/root.css @@ -1,3 +1,22 @@ +:root { + --intro-bg-img: url('/img/intro-header.jpg'); + --intro-bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg')); + --intro-bg-img-sm: url('/img/intro-header-sm.jpg'); + --intro-bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg')); + --intro-bg-color: #8c5919; + --intro-bg-block-1: #b689ff; + --intro-bg-block-2: #fecaca; + --intro-bg-block-3: #e6a9e2; + --intro-bg-block-4: #ffedca; + --intro-bg-block-5: #ffffff; + --intro-color: #1a202c; + --intro-color-gray: #e4e4e7; + --intro-color-link: #1e4eae; + --intro-focus-outline: 2px solid var(--intro-color-link); + --intro-focus-outline-offset: 2px; + --intro-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); +} + :root { --val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; --val-font-serif: "Lora","georgia",serif; diff --git a/static/js/menu.js b/static/js/menu.js index 6b5ae1b..1f09bfe 100644 --- a/static/js/menu.js +++ b/static/js/menu.js @@ -48,7 +48,6 @@ function menu__reset(menu, nav, overlay) { } document.querySelectorAll('.menu').forEach(menu => { - let menuChildren = []; const menuNav = menu.querySelector('.menu__nav'); const menuOverlay = menu.querySelector('.menu__overlay');