diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 709ce57..5af5f9c 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -266,13 +266,10 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { v }; - // Filtra los atributos descartando `#[doc]` y `#[inline]` para el método `alter_...()`. - let non_doc_or_inline_attrs: Vec<_> = attrs + // Extrae atributos descartando la documentación para incluir en `alter_...()`. + let non_doc_attrs: Vec<_> = attrs .iter() - .filter(|a| { - let p = a.path(); - !p.is_ident("doc") && !p.is_ident("inline") - }) + .filter(|&a| !a.path().is_ident("doc")) .cloned() .collect(); @@ -287,21 +284,14 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { #(#attrs)* fn #with_name #generics (self, #(#args),*) -> Self #where_clause; - #(#non_doc_or_inline_attrs)* + #(#non_doc_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),*); @@ -310,7 +300,6 @@ 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 @@ -321,7 +310,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream { #(#attrs)* #with_fn - #(#non_doc_or_inline_attrs)* + #(#non_doc_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 c9f7903..7a36fbb 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 class="menu__link" href=(path(cx)) title=[description] { + a 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 class="menu__link" href=(path(cx)) title=[description] target="_blank" { + a 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" { - button type="button" class="menu__link" title=[description] { + a href="#" 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" { - button type="button" class="menu__link" title=[description] { + a href="#" 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 a4b2df5..40129bf 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, BasicRegion}; +pub use basic::Basic; diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index b6a982f..ecd6485 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -1,9 +1,6 @@ /// 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*): @@ -49,8 +46,12 @@ 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") @@ -61,11 +62,6 @@ 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) @@ -76,6 +72,11 @@ 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) @@ -85,52 +86,42 @@ 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! { - header class="intro-header" { - section class="intro-header__body" { - h1 class="intro-header__title" { - span { (title) } - (intro) + 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) + } } - } - 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"; + } } } } - (h) - } - main class="intro-content" { - section class="intro-content__body" { - div class="intro-text" { + main class="intro-content" { + section class="intro-content__body" { @if intro_button_lnk.is_some() { div class="intro-button" { a @@ -146,34 +137,33 @@ fn render_intro(page: &mut Page) -> Markup { } } } - (c) + div class="intro-text" { (page.render_region("content")) } } } - } - 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" {} + 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)) } } } - 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 f58d381..9dad7f5 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -1,6 +1,5 @@ -use crate::core::component::ChildOp; use crate::core::theme::all::{theme_by_short_name, DEFAULT_THEME}; -use crate::core::theme::{ChildrenInRegions, ThemeRef}; +use crate::core::theme::ThemeRef; use crate::core::TypeInfo; use crate::html::{html, Markup}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; @@ -105,10 +104,6 @@ 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. @@ -216,7 +211,6 @@ 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. } @@ -256,7 +250,6 @@ impl Context { favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), - regions : ChildrenInRegions::default(), params : HashMap::default(), id_counter : 0, } @@ -290,13 +283,6 @@ 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. @@ -485,12 +471,6 @@ 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 64f40f3..61d820b 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -15,10 +15,10 @@ //! [`Theme`]. mod definition; -pub use definition::{Theme, ThemePage, ThemeRef, ThemeRegion}; +pub use definition::{Theme, ThemePage, ThemeRef}; mod regions; pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; -pub use regions::{InRegion, Region, RegionRef}; +pub use regions::{InRegion, Region}; pub(crate) mod all; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 7ef95c4..38a0bfc 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, RegionRef, REGION_CONTENT}; +use crate::core::theme::Region; +use crate::global; use crate::html::{html, Markup}; use crate::locale::L10n; use crate::response::page::Page; -use crate::{global, join}; use std::sync::LazyLock; @@ -13,138 +13,78 @@ 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 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. +/// 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. /// -/// 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*): +/// 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*): /// /// - `::render_body(self, page, self.page_regions())` /// - `::render_head(self, page)` pub trait ThemePage { - /// Renderiza el **contenedor** de una región concreta del `` de la página. + /// Renderiza el contenido del `` de la página. /// - /// 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 { + /// 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 { html! { - @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) + 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) + } + } } } } } - /// Renderiza el **contenido interior** del `` de la página. + /// Renderiza el contenido del `` de la página. /// - /// 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`, + /// Por defecto incluye 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! { - meta charset="utf-8"; + head { + 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(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(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()) } } } @@ -185,53 +125,31 @@ pub trait Theme: Extension + ThemePage + Send + Sync { /// Declaración ordenada de las regiones disponibles en la página. /// - /// Retorna una **lista estática** de referencias ([`RegionRef`](crate::core::theme::RegionRef)) - /// que representan las regiones que el tema admite dentro del ``. + /// 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 . /// - /// 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: + /// Si un tema necesita un conjunto distinto de regiones, se puede **sobrescribir** este método + /// con los siguientes requisitos y recomendaciones: /// - /// - 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. + /// - 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. /// - /// # Ejemplo + /// Por defecto devuelve: /// - /// ```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(|| { + /// - `"header"`: cabecera. + /// - `"content"`: contenido principal (**obligatoria**). + /// - `"footer"`: pie. + fn page_regions(&self) -> &'static [(Region, L10n)] { + static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { [ - &ThemeRegion::Header, - &ThemeRegion::Content, - &ThemeRegion::Footer, + (Region::declare("header"), L10n::l("region_header")), + (Region::default(), L10n::l("region_content")), + (Region::declare("footer"), L10n::l("region_footer")), ] }); - &*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) + ®IONS[..] } /// 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 ecb5eb5..8082aac 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -1,6 +1,5 @@ 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; @@ -19,95 +18,88 @@ static COMMON_REGIONS: LazyLock> = /// Nombre de la región de contenido por defecto (`"content"`). pub const REGION_CONTENT: &str = "content"; -/// Define la interfaz mínima que describe una **región de renderizado** dentro de una página. +/// Identificador de una región de página. /// -/// 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. +/// 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}`). /// -/// 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. - /// - /// 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 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; +/// 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, } -/// Referencia estática a una región. -pub type RegionRef = &'static dyn Region; +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. + /// + /// 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 (`-`). + /// + /// 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 + } +} // Contenedor interno de componentes agrupados por región. #[derive(AutoDefault)] pub struct ChildrenInRegions(HashMap<&'static str, Children>); impl ChildrenInRegions { - pub fn with(region_key: &'static str, child: Child) -> Self { - ChildrenInRegions::default().with_child_in(region_key, ChildOp::Add(child)) + pub fn with(region_name: &'static str, child: Child) -> Self { + ChildrenInRegions::default().with_child_in(region_name, ChildOp::Add(child)) } #[builder_fn] - pub fn with_child_in(mut self, region_key: &'static str, op: ChildOp) -> Self { - if let Some(region) = self.0.get_mut(region_key) { + pub fn with_child_in(mut self, region_name: &'static str, op: ChildOp) -> Self { + if let Some(region) = self.0.get_mut(region_name) { region.alter_child(op); } else { - self.0.insert(region_key, Children::new().with_child(op)); + self.0.insert(region_name, Children::new().with_child(op)); } self } - pub fn merge_all_components(&self, theme_ref: ThemeRef, region_key: &'static str) -> Children { + pub fn merge_all_components(&self, theme_ref: ThemeRef, region_name: &'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_key), - self.0.get(region_key), - r.0.get(region_key), + common.0.get(region_name), + self.0.get(region_name), + r.0.get(region_name), ]) } else { - Children::merge(&[common.0.get(region_key), self.0.get(region_key)]) + Children::merge(&[common.0.get(region_name), self.0.get(region_name)]) } } } @@ -122,9 +114,9 @@ impl ChildrenInRegions { pub enum InRegion { /// Región de contenido por defecto. Content, - /// Región identificada por la clave proporcionado. - Key(&'static str), - /// Región identificada por una clave para un tema concreto. + /// Región identificada por el nombre proporcionado. + Named(&'static str), + /// Región identificada por un nombre y asociada a un tema concreto. OfTheme(&'static str, ThemeRef), } @@ -142,7 +134,7 @@ impl InRegion { /// ))); /// /// // Texto en la región "sidebar". - /// InRegion::Key("sidebar").add(Child::with(Html::with(|_| + /// InRegion::Named("sidebar").add(Child::with(Html::with(|_| /// html! { ("Publicidad") } /// ))); /// ``` @@ -153,19 +145,19 @@ impl InRegion { .write() .alter_child_in(REGION_CONTENT, ChildOp::Add(child)); } - InRegion::Key(region_key) => { + InRegion::Named(region_name) => { COMMON_REGIONS .write() - .alter_child_in(region_key, ChildOp::Add(child)); + .alter_child_in(region_name, ChildOp::Add(child)); } - InRegion::OfTheme(region_key, theme_ref) => { + InRegion::OfTheme(region_name, theme_ref) => { let mut regions = THEME_REGIONS.write(); if let Some(r) = regions.get_mut(&theme_ref.type_id()) { - r.alter_child_in(region_key, ChildOp::Add(child)); + r.alter_child_in(region_name, ChildOp::Add(child)); } else { regions.insert( theme_ref.type_id(), - ChildrenInRegions::with(region_key, child), + ChildrenInRegions::with(region_name, child), ); } } diff --git a/src/response/page.rs b/src/response/page.rs index 1649d54..f81c980 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::{ThemeRef, REGION_CONTENT}; +use crate::core::theme::{ChildrenInRegions, ThemeRef, REGION_CONTENT}; use crate::html::{html, Markup, DOCTYPE}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; use crate::html::{AttrClasses, ClassesOp}; @@ -29,6 +29,7 @@ pub struct Page { body_id : AttrId, body_classes: AttrClasses, context : Context, + regions : ChildrenInRegions, } impl Page { @@ -46,6 +47,7 @@ impl Page { body_id : AttrId::default(), body_classes: AttrClasses::default(), context : Context::new(Some(request)), + regions : ChildrenInRegions::default(), } } @@ -103,37 +105,48 @@ 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_key: &'static str, component: impl Component) -> Self { - self.add_component_in(region_key, component) + pub fn with_component_in(self, region_name: &'static str, component: impl Component) -> Self { + self.add_component_in(region_name, component) } /// Añade un componente a la región de contenido por defecto. pub fn add_component(mut self, component: impl Component) -> Self { - self.context + self.regions .alter_child_in(REGION_CONTENT, ChildOp::Add(Child::with(component))); self } - /// 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))); + /// 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))); 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_key: &'static str, op: ChildOp) -> Self { - self.alter_child_in(region_key, op); + pub fn with_child_in_region(mut self, region_name: &'static str, op: ChildOp) -> Self { + self.alter_child_in(region_name, 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_key: &'static str, op: ChildOp) -> &mut Self { - self.alter_child_in(region_key, op); + 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); self } @@ -180,6 +193,18 @@ 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 ``, @@ -213,14 +238,8 @@ impl Page { Ok(html! { (DOCTYPE) html lang=(lang) dir=(dir) { - 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")) - } + (head) + (body) } }) } @@ -271,12 +290,6 @@ 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 058e173..04801dd 100644 --- a/static/css/basic.css +++ b/static/css/basic.css @@ -1,19 +1,3 @@ -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 774bbb2..39c9d6a 100644 --- a/static/css/intro.css +++ b/static/css/intro.css @@ -1,15 +1,37 @@ +: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; - color: var(--intro-color); - background-color: var(--intro-bg-color); + 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; width: 100%; display: flex; @@ -29,12 +51,12 @@ a { transition: font-size 0.2s, text-decoration-color 0.2s; } a:focus-visible { - outline: var(--intro-focus-outline); - outline-offset: var(--intro-focus-outline-offset); + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); } a:hover, a:hover:visited { - text-decoration-color: var(--intro-color-link); + text-decoration-color: var(--color-link); } /* @@ -47,9 +69,9 @@ a:hover:visited { width: 100%; max-width: 80rem; margin: 0 auto; - padding-bottom: 4rem; - background-image: var(--intro-bg-img-sm); - background-image: var(--intro-bg-img-sm-set); + padding-bottom: 9rem; + background-image: var(--bg-img-sm); + background-image: var(--bg-img-sm-set); background-position: top center; background-position-y: -1rem; background-size: contain; @@ -97,8 +119,8 @@ a:hover:visited { } @media (min-width: 64rem) { .intro-header { - background-image: var(--intro-bg-img); - background-image: var(--intro-bg-img-set); + background-image: var(--bg-img); + background-image: var(--bg-img-set); } .intro-header__title { padding: 1.2rem 2rem 2.6rem 2rem; @@ -158,7 +180,8 @@ a:hover:visited { .intro-button { width: 100%; - margin: 0 auto; + margin: 0 auto 3rem; + z-index: 10; } .intro-button__link { background: #7f1d1d; @@ -283,9 +306,6 @@ 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; @@ -322,15 +342,17 @@ 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; } -.region--content p { +.intro-button + .intro-text { + padding-top: 6rem; +} +.intro-text p { width: 100%; line-height: 150%; font-weight: 400; @@ -342,39 +364,31 @@ a:hover:visited { font-size: 1.375rem; line-height: 2rem; } - .intro-button + .region--content { + .intro-button + .intro-text { padding-top: 7rem; } } @media (min-width: 64rem) { - .intro-header { - padding-bottom: 9rem;; - } - .intro-text, - .region--content { - border-radius: 0.75rem; - } .intro-text { - box-shadow: var(--intro-shadow); + border-radius: 0.75rem; + box-shadow: var(--shadow); max-width: 60rem; margin: 0 auto 6rem; - } - .region--content { padding-left: 4.5rem; padding-right: 4.5rem; } } -.region--content .block { +.intro-text .block { position: relative; } -.region--content .block__title { +.intro-text .block__title { margin: 1em 0 .8em; } -.region--content .block__title span { +.intro-text .block__title span { display: inline-block; padding: 10px 30px 14px; - margin: 30px 0 0 20px; + margin: 0 0 0 20px; background: white; border: 5px solid; border-radius: 30px; @@ -382,7 +396,7 @@ a:hover:visited { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.region--content .block__title:before { +.intro-text .block__title:before { content: ""; height: 5px; position: absolute; @@ -395,7 +409,7 @@ a:hover:visited { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.region--content .block__title:after { +.intro-text .block__title:after { content: ""; height: 70rem; position: absolute; @@ -403,23 +417,23 @@ a:hover:visited { left: -15%; width: 130%; z-index: -10; - background: var(--intro-bg-block-1); + background: var(--color-block-1); transform: rotate(2deg); } -.region--content .block:nth-of-type(5n+1) .block__title:after { - background: var(--intro-bg-block-1); +.intro-text .block:nth-of-type(5n+1) .block__title:after { + background: var(--color-block-1); } -.region--content .block:nth-of-type(5n+2) .block__title:after { - background: var(--intro-bg-block-2); +.intro-text .block:nth-of-type(5n+2) .block__title:after { + background: var(--color-block-2); } -.region--content .block:nth-of-type(5n+3) .block__title:after { - background: var(--intro-bg-block-3); +.intro-text .block:nth-of-type(5n+3) .block__title:after { + background: var(--color-block-3); } -.region--content .block:nth-of-type(5n+4) .block__title:after { - background: var(--intro-bg-block-4); +.intro-text .block:nth-of-type(5n+4) .block__title:after { + background: var(--color-block-4); } -.region--content .block:nth-of-type(5n+5) .block__title:after { - background: var(--intro-bg-block-5); +.intro-text .block:nth-of-type(5n+5) .block__title:after { + background: var(--color-block-5); } /* @@ -429,7 +443,7 @@ a:hover:visited { .intro-footer { width: 100%; background-color: black; - color: var(--intro-color-gray); + color: var(--color-gray); padding-bottom: 2rem; } @@ -445,7 +459,7 @@ a:hover:visited { line-height: 100%; } .intro-footer__body a:visited { - color: var(--intro-color-gray); + color: var(--color-gray); } .intro-footer__logo, .intro-footer__links { @@ -478,5 +492,5 @@ a:hover:visited { /* PoweredBy component */ .poweredby a:visited { - color: var(--intro-color-gray); + color: var(--color-gray); } diff --git a/static/css/menu.css b/static/css/menu.css index 6522f4a..5520e39 100644 --- a/static/css/menu.css +++ b/static/css/menu.css @@ -1,92 +1,54 @@ -/* 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; + border: none; + outline: none; 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; - margin-inline-start: 1.5rem; + margin: 0 0 0 1.5rem; padding: 0; line-height: var(--val-menu--item-height); list-style: none; + list-style-type: none; } .menu__item--label, -.menu__nav li > .menu__link { +.menu__nav li > a { position: relative; - font-weight: normal; + font-weight: 500; + color: var(--val-color--text); text-rendering: optimizeLegibility; - font-size: 1.45rem; } -.menu__nav li > .menu__link { +.menu__nav li > a { + border: none; transition: color 0.3s ease-in-out; } -.menu__nav li:hover > .menu__link, -.menu__nav li > .menu__link:focus { +.menu__nav li:hover > a, +.menu__nav li > a:focus { color: var(--val-menu--color-highlight); } -.menu__nav li > .menu__link > svg.icon { +.menu__nav li > a > svg.icon { margin-left: 0.25rem; } @@ -95,18 +57,19 @@ max-width: 100%; height: auto; padding: var(--val-gap-0-5) var(--val-gap-1-5); - border: 0; + border: none; + outline: none; 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; + transition: all 0.5s ease-in-out; } .menu__item--children:hover > .menu__children, -.menu__item--children > .menu__link:focus + .menu__children, +.menu__item--children > a:focus + .menu__children, .menu__item--children .menu__children:focus-within { margin-top: 0.4rem; opacity: 1; @@ -118,12 +81,14 @@ max-width: var(--val-menu--item-width-max); } .menu__submenu-title { + font-family: inherit; font-size: 1rem; - font-weight: normal; + font-weight: 500; margin: 0; padding: var(--val-menu--line-padding) 0; line-height: var(--val-menu--line-height); - border: 0; + border: none; + outline: none; color: var(--val-menu--color-highlight); text-transform: uppercase; text-rendering: optimizeLegibility; @@ -148,15 +113,18 @@ display: none; } -/* Responsive <= 62rem (992px) */ - -@media (max-width: 62rem) { +/* Applies <= 992px */ +@media only screen and (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; @@ -165,13 +133,6 @@ width: 2rem; height: 2rem; } - - .menu__nav, - .menu__children { - overscroll-behavior: contain; - -webkit-overflow-scrolling: touch; - } - .menu__nav { position: fixed; top: 0; @@ -182,16 +143,10 @@ 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; + transition: all 0.5s ease-in-out; } .menu__nav.active { transform: translate(0%); - visibility: visible; - pointer-events: auto; } .menu__nav li { @@ -201,18 +156,16 @@ } .menu__item--label, - .menu__nav li > .menu__link { + .menu__nav li > a { 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 { + .menu__nav li ul li > a { border-bottom: 0; } - .menu__nav li > .menu__link > svg.icon { + .menu__nav li > a > svg.icon { position: absolute; top: var(--val-menu--line-padding); right: var(--val-menu--line-padding); @@ -238,7 +191,6 @@ 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; @@ -271,16 +223,6 @@ 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 { @@ -289,20 +231,18 @@ 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: 1px solid var(--val-menu--color-border) !important; - border-width: 0 0 1px 1px !important; + border-left: 1px solid var(--val-menu--color-border) !important; } .menu__back { font-size: 1.25rem; - border: 1px solid var(--val-menu--color-border) !important; - border-width: 0 1px 1px 0 !important; + border-right: 1px solid var(--val-menu--color-border) !important; display: none; } .menu__header.active .menu__back { @@ -327,34 +267,15 @@ opacity: 0; visibility: hidden; background: rgba(0, 0, 0, 0.55); - transition: opacity .5s ease-in-out, visibility 0s linear .5s; + transition: all 0.5s ease-in-out; } .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; - } -} - -@media (prefers-reduced-motion: reduce) { - .menu__nav, - .menu__children, - .menu__title, - .menu__overlay { - transition: none !important; - animation: none !important; - } -} - -/* Animaciones */ +/* ANIMATIONS */ @keyframes slideLeft { 0% { diff --git a/static/css/root.css b/static/css/root.css index 270c1b3..aeab1c6 100644 --- a/static/css/root.css +++ b/static/css/root.css @@ -1,22 +1,3 @@ -: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 1f09bfe..6b5ae1b 100644 --- a/static/js/menu.js +++ b/static/js/menu.js @@ -48,6 +48,7 @@ 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');