From 8274519405382c52070f6dc68fea4a8caffc029b Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 4 Sep 2025 01:53:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20[welcome]=20Crea=20p=C3=A1gina?= =?UTF-8?q?=20de=20bienvenida=20desde=20intro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implementa un nuevo *layout* en el tema `Basic` para crear una plantilla de páginas de introducción. - Añade nuevo fichero CSS `intro.css` para los estilos globales de la página de introducción. - Incorpora nuevos recursos gráficos para la cabecera de la página de introducción en varios formatos (AVIF, WebP, JPEG). - Revisa los ficheros de localización. --- src/base/component.rs | 3 + src/base/component/block.rs | 103 +++++++++ src/base/extension/welcome.rs | 105 ++------- src/base/theme/basic.rs | 91 +++++++- src/core/component/definition.rs | 6 +- src/core/theme.rs | 6 +- src/core/theme/definition.rs | 210 +++++++++++------- src/core/theme/regions.rs | 2 +- src/locale/en-US/welcome.ftl | 1 - src/locale/es-ES/welcome.ftl | 1 - static/css/{welcome.css => intro.css} | 150 ++++++------- ...me-header-sm.avif => intro-header-sm.avif} | Bin ...come-header-sm.jpg => intro-header-sm.jpg} | Bin ...me-header-sm.webp => intro-header-sm.webp} | Bin ...{welcome-header.avif => intro-header.avif} | Bin .../{welcome-header.jpg => intro-header.jpg} | Bin ...{welcome-header.webp => intro-header.webp} | Bin 17 files changed, 420 insertions(+), 258 deletions(-) create mode 100644 src/base/component/block.rs rename static/css/{welcome.css => intro.css} (78%) rename static/img/{welcome-header-sm.avif => intro-header-sm.avif} (100%) rename static/img/{welcome-header-sm.jpg => intro-header-sm.jpg} (100%) rename static/img/{welcome-header-sm.webp => intro-header-sm.webp} (100%) rename static/img/{welcome-header.avif => intro-header.avif} (100%) rename static/img/{welcome-header.jpg => intro-header.jpg} (100%) rename static/img/{welcome-header.webp => intro-header.webp} (100%) diff --git a/src/base/component.rs b/src/base/component.rs index 30cb686..4df64ff 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -3,5 +3,8 @@ mod html; pub use html::Html; +mod block; +pub use block::Block; + mod poweredby; pub use poweredby::PoweredBy; diff --git a/src/base/component/block.rs b/src/base/component/block.rs new file mode 100644 index 0000000..c96f2ba --- /dev/null +++ b/src/base/component/block.rs @@ -0,0 +1,103 @@ +use crate::prelude::*; + +/// Componente genérico que representa un bloque de contenido. +/// +/// Los bloques se utilizan como contenedores de otros componentes o contenidos, con un título +/// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*). +#[rustfmt::skip] +#[derive(AutoDefault)] +pub struct Block { + id : AttrId, + classes : AttrClasses, + title : L10n, + children: Children, +} + +impl Component for Block { + fn new() -> Self { + Block::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + fn setup_before_prepare(&mut self, _cx: &mut Context) { + self.alter_classes(ClassesOp::Prepend, "block"); + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let block_body = self.children().render(cx); + + if block_body.is_empty() { + return PrepareMarkup::None; + } + + let id = cx.required_id::(self.id()); + + PrepareMarkup::With(html! { + div id=(id) class=[self.classes().get()] { + @if let Some(title) = self.title().lookup(cx) { + h2 class="block__title" { span { (title) } } + } + div class="block__body" { (block_body) } + } + }) + } +} + +impl Block { + // Block BUILDER ******************************************************************************* + + /// Establece el identificador único (`id`) del bloque. + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_value(id); + self + } + + /// Modifica la lista de clases CSS aplicadas al bloque. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_value(op, classes); + self + } + + /// Establece el título del bloque. + #[builder_fn] + pub fn with_title(mut self, title: L10n) -> Self { + self.title = title; + self + } + + /// Añade un nuevo componente hijo al bloque. + pub fn add_component(mut self, component: impl Component) -> Self { + self.children + .alter_child(ChildOp::Add(Child::with(component))); + self + } + + /// Modifica la lista de hijos (`children`) aplicando una operación. + #[builder_fn] + pub fn with_child(mut self, op: ChildOp) -> Self { + self.children.alter_child(op); + self + } + + // Block GETTERS ******************************************************************************* + + /// Devuelve las clases CSS asociadas al bloque. + pub fn classes(&self) -> &AttrClasses { + &self.classes + } + + /// Devuelve el título del bloque como [`L10n`]. + pub fn title(&self) -> &L10n { + &self.title + } + + /// Devuelve la lista de hijos (`children`) del bloque. + pub fn children(&self) -> &Children { + &self.children + } +} diff --git a/src/base/extension/welcome.rs b/src/base/extension/welcome.rs index 0252cff..3c0de3d 100644 --- a/src/base/extension/welcome.rs +++ b/src/base/extension/welcome.rs @@ -25,95 +25,26 @@ async fn homepage(request: HttpRequest) -> ResultPage { let app = &global::SETTINGS.app.name; Page::new(Some(request)) - .with_title(L10n::l("welcome_page")) - .with_theme("Basic") - .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css"))) - .with_body_classes(ClassesOp::Add, "welcome") - .with_component_in("header", Html::with(move |cx| html! { - div class="welcome-header" { - header class="welcome-header__body" { - h1 - class="welcome-header__title" - aria-label=(L10n::l("welcome_aria").with_arg("app", app).to_markup(cx)) - { - span { (L10n::l("welcome_title").to_markup(cx)) } - (L10n::l("welcome_intro").with_arg("app", app).to_markup(cx)) - } - } - aside class="welcome-header__image" aria-hidden="true" { - div class="welcome-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"; - } - } - } + .with_theme("basic") + .with_layout("intro") + .with_title(L10n::l("welcome_title")) + .with_description(L10n::l("welcome_intro").with_arg("app", app)) + .add_component(Html::with(|cx| { + html! { + p { (L10n::l("welcome_text1").using(cx)) } + p { (L10n::l("welcome_text2").using(cx)) } } })) - .with_component(Html::with(move |cx| html! { - main class="welcome-content" { - section class="welcome-content__body" { - div class="welcome-poweredby" { - a - class="welcome-poweredby__link" - href="https://pagetop.cillero.es" - target="_blank" - rel="noreferrer" - { - span {} span {} span {} - div class="welcome-poweredby__text" { - (L10n::l("welcome_powered").to_markup(cx)) - } - } + .add_component( + Block::new() + .with_title(L10n::l("welcome_about")) + .add_component(Html::with(move |cx| { + html! { + p { (L10n::l("welcome_pagetop").using(cx)) } + p { (L10n::l("welcome_issues1").using(cx)) } + p { (L10n::l("welcome_issues2").with_arg("app", app).using(cx)) } } - div class="welcome-text" { - p { (L10n::l("welcome_text1").to_markup(cx)) } - p { (L10n::l("welcome_text2").to_markup(cx)) } - - div class="welcome-text__block" { - h2 { span { (L10n::l("welcome_about").to_markup(cx)) } } - p { (L10n::l("welcome_pagetop").to_markup(cx)) } - p { (L10n::l("welcome_issues1").to_markup(cx)) } - p { (L10n::l("welcome_issues2").with_arg("app", app).to_markup(cx)) } - } - } - } - } - })) - .with_component_in("footer", Html::with(move |cx| html! { - section class="welcome-footer" { - div class="welcome-footer__logo" { - svg - viewBox="0 0 1614 1614" - xmlns="http://www.w3.org/2000/svg" - role="img" - aria-label=[L10n::l("pagetop_logo").using(cx)] - 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="welcome-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("welcome_code").to_markup(cx)) } - em { (L10n::l("welcome_have_fun").to_markup(cx)) } - } - } - })) - .with_component_in("footer", PoweredBy::new()) + })), + ) .render() } diff --git a/src/base/theme/basic.rs b/src/base/theme/basic.rs index 961864b..dc16f2a 100644 --- a/src/base/theme/basic.rs +++ b/src/base/theme/basic.rs @@ -12,16 +12,105 @@ impl Extension for Basic { } impl Theme for Basic { + fn render_page_body(&self, page: &mut Page) -> Markup { + match page.layout() { + "intro" => render_intro(page), + _ => ::render_body(self, page, self.page_regions()), + } + } + fn after_render_page_body(&self, page: &mut Page) { + let styles = match page.layout() { + "intro" => "/css/intro.css", + _ => "/css/basic.css", + }; page.alter_assets(AssetsOp::AddStyleSheet( StyleSheet::from("/css/normalize.css") .with_version("8.0.1") .with_weight(-99), )) .alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/css/basic.css") + StyleSheet::from(styles) .with_version(env!("CARGO_PKG_VERSION")) .with_weight(-99), )); } } + +fn render_intro(page: &mut Page) -> Markup { + let title = page.title().unwrap_or_default(); + let intro = page.description().unwrap_or_default(); + + 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) + } + } + 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" { + div class="intro-button" { + a + class="intro-button__link" + href="https://pagetop.cillero.es" + target="_blank" + rel="noreferrer" + { + span {} span {} span {} + div class="intro-button__text" { + (L10n::l("welcome_powered").using(page)) + } + } + } + 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" {} + } + } + 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("welcome_code").using(page)) } + em { (L10n::l("welcome_have_fun").using(page)) } + } + } + } + } + } +} diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index c43dfb0..d547c4b 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -29,14 +29,14 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync { TypeInfo::ShortName.of::() } - /// Devuelve una descripción opcional del componente. + /// Devuelve una descripción del componente, si existe. /// /// Por defecto, no se proporciona ninguna descripción (`None`). fn description(&self) -> Option { None } - /// Devuelve un identificador opcional para el componente. + /// Devuelve el identificador del componente, si existe. /// /// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no /// tiene ningún identificador (`None`). @@ -51,7 +51,7 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync { #[allow(unused_variables)] fn setup_before_prepare(&mut self, cx: &mut Context) {} - /// Devuelve una representación estructurada del componente preparada para el renderizado. + /// Devuelve una representación renderizada del componente. /// /// Este método forma parte del ciclo de vida de los componentes y se invoca automáticamente /// durante el proceso de construcción del documento. Puede sobrescribirse para generar diff --git a/src/core/theme.rs b/src/core/theme.rs index e0c3008..5889dcf 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -15,10 +15,10 @@ //! [`Theme`]. mod definition; -pub use definition::{Theme, ThemeRef}; +pub use definition::{Theme, ThemePage, ThemeRef}; mod regions; -pub(crate) use regions::ChildrenInRegions; -pub use regions::{InRegion, Region, REGION_CONTENT}; +pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT}; +pub use regions::{InRegion, Region}; pub(crate) mod all; diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 3b26a57..a4faf0b 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -7,88 +7,34 @@ use crate::response::page::Page; use std::sync::LazyLock; -/// Representa una referencia a un tema. +/// Referencia estática a un tema. /// -/// Los temas son también extensiones. Por tanto se deben definir igual, es decir, como instancias -/// estáticas globales que implementan [`Theme`], pero también [`Extension`]. +/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que +/// implementen [`Theme`] y, a su vez, [`Extension`]. pub type ThemeRef = &'static dyn Theme; -/// Interfaz común que debe implementar cualquier tema de `PageTop`. +/// Métodos predefinidos de renderizado para las páginas de un tema. /// -/// Un tema implementará [`Theme`] y los métodos que sean necesarios de [`Extension`], aunque el -/// único obligatorio será [`theme()`](Extension::theme). +/// 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. /// -/// ```rust -/// use pagetop::prelude::*; +/// 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*): /// -/// pub struct MyTheme; -/// -/// impl Extension for MyTheme { -/// fn name(&self) -> L10n { -/// L10n::n("My theme") -/// } -/// -/// fn description(&self) -> L10n { -/// L10n::n("A personal theme") -/// } -/// -/// fn theme(&self) -> Option { -/// Some(&Self) -/// } -/// } -/// -/// impl Theme for MyTheme {} -/// ``` -pub trait Theme: Extension + Send + Sync { - /// **Obsoleto desde la versión 0.4.0**: usar [`declared_regions()`](Self::declared_regions) en - /// su lugar. - #[deprecated(since = "0.4.0", note = "Use `declared_regions()` instead")] - fn regions(&self) -> Vec<(&'static str, L10n)> { - vec![("content", L10n::l("content"))] - } - - /// 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 en el - /// orden indicado todas las regiones que componen una página. Los identificadores deben ser - /// **estables** como `"sidebar-left"` o `"content"`. La etiqueta `L10n` devuelve el nombre de la - /// región en el idioma activo de la página. - /// - /// Si el tema requiere un conjunto distinto de regiones, se puede sobrescribir este método para - /// devolver una lista diferente. Si no, se usará la lista predeterminada: - /// - /// - `"header"`: cabecera. - /// - `"content"`: contenido principal (**obligatoria**). - /// - `"footer"`: pie. - /// - /// Sólo la región `"content"` es obligatoria, usa [`Region::default()`] para declararla. - #[inline] - fn declared_regions(&self) -> &'static [(Region, L10n)] { - static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { - [ - (Region::declare("header"), L10n::l("region_header")), - (Region::default(), L10n::l("region_content")), - (Region::declare("footer"), L10n::l("region_footer")), - ] - }); - ®IONS[..] - } - - /// Acciones específicas del tema antes de renderizar el `` de la página. - /// - /// Útil para preparar clases, inyectar recursos o ajustar metadatos. - #[allow(unused_variables)] - fn before_render_page_body(&self, page: &mut Page) {} - +/// - `::render_body(self, page, self.page_regions())` +/// - `::render_head(self, page)` +pub trait ThemePage { /// Renderiza el contenido del `` de la página. /// - /// Por defecto, recorre [`declared_regions()`](Self::declared_regions) **en el orden que se han - /// declarado** y, para cada región con contenido, genera un contenedor con `role="region"` y - /// `aria-label` localizado. - fn render_page_body(&self, page: &mut Page) -> 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! { body id=[page.body_id().get()] class=[page.body_classes().get()] { - @for (region, region_label) in self.declared_regions() { + @for (region, region_label) in regions { @let output = page.render_region(region.key()); @if !output.is_empty() { @let region_name = region.name(); @@ -96,7 +42,7 @@ pub trait Theme: Extension + Send + Sync { id=(region_name) class={ "region region--" (region_name) } role="region" - aria-label=[region_label.using(page)] + aria-label=[region_label.lookup(page)] { (output) } @@ -106,17 +52,12 @@ pub trait Theme: Extension + Send + Sync { } } - /// Acciones específicas del tema después de renderizar el `` de la página. - /// - /// Útil para *tracing*, métricas o ajustes finales del estado de la página. - #[allow(unused_variables)] - fn after_render_page_body(&self, page: &mut Page) {} - /// Renderiza el contenido del `` de la página. /// - /// Por defecto, genera las etiquetas básicas (`charset`, `title`, `description`, `viewport`, - /// `X-UA-Compatible`), los metadatos y propiedades de la página y los recursos (CSS/JS). - fn render_page_head(&self, page: &mut Page) -> Markup { + /// 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. + fn render_head(&self, page: &mut Page) -> Markup { let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no"; html! { head { @@ -146,18 +87,115 @@ pub trait Theme: Extension + Send + Sync { } } } +} - /// Página de error "*403 – Forbidden*" predeterminada. +/// Interfaz común que debe implementar cualquier tema de PageTop. +/// +/// Un tema implementa [`Theme`] y los métodos necesarios de [`Extension`]. El único método +/// **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme). +/// +/// ```rust +/// use pagetop::prelude::*; +/// +/// pub struct MyTheme; +/// +/// impl Extension for MyTheme { +/// fn name(&self) -> L10n { +/// L10n::n("My theme") +/// } +/// +/// fn description(&self) -> L10n { +/// L10n::n("A personal theme") +/// } +/// +/// fn theme(&self) -> Option { +/// Some(&Self) +/// } +/// } +/// +/// impl Theme for MyTheme {} +/// ``` +pub trait Theme: Extension + ThemePage + Send + Sync { + /// **Obsoleto desde la versión 0.4.0**: usar [`page_regions()`](Self::page_regions) en su + /// lugar. + #[deprecated(since = "0.4.0", note = "Use `page_regions()` instead")] + fn regions(&self) -> Vec<(&'static str, L10n)> { + vec![("content", L10n::l("content"))] + } + + /// 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 en el + /// orden indicado todas las regiones que componen una página. + /// + /// Requisitos y 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. + /// + /// Si tu tema define un conjunto distinto, se puede **sobrescribir** este método. Por defecto + /// devuelve: + /// + /// - `"header"`: cabecera. + /// - `"content"`: contenido principal (**obligatoria**). + /// - `"footer"`: pie. + fn page_regions(&self) -> &'static [(Region, L10n)] { + static REGIONS: LazyLock<[(Region, L10n); 3]> = LazyLock::new(|| { + [ + (Region::declare("header"), L10n::l("region_header")), + (Region::default(), L10n::l("region_content")), + (Region::declare("footer"), L10n::l("region_footer")), + ] + }); + ®IONS[..] + } + + /// Acciones específicas del tema antes de renderizar el `` de la página. + /// + /// Útil para preparar clases, inyectar recursos o ajustar metadatos. + #[allow(unused_variables)] + fn before_render_page_body(&self, page: &mut Page) {} + + /// Renderiza el contenido del `` de la página. + /// + /// Si se sobrescribe este método, se puede volver al comportamiento base con: + /// `::render_body(self, page, self.page_regions())`. + #[inline] + fn render_page_body(&self, page: &mut Page) -> Markup { + ::render_body(self, page, self.page_regions()) + } + + /// Acciones específicas del tema después de renderizar el `` de la página. + /// + /// Útil para *tracing*, métricas o ajustes finales del estado de la página. + #[allow(unused_variables)] + fn after_render_page_body(&self, page: &mut Page) {} + + /// Renderiza el contenido del `` de la página. + /// + /// Si se sobrescribe este método, se puede volver al comportamiento base con: + /// `::render_head(self, page)`. + #[inline] + fn render_page_head(&self, page: &mut Page) -> Markup { + ::render_head(self, page) + } + + /// Contenido predeterminado para la página de error "*403 – Forbidden*". /// /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. fn error403(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error403_notice").to_markup(page)) } } } + html! { div { h1 { (L10n::l("error403_notice").using(page)) } } } } - /// Página de error "*404 – Not Found*" predeterminada. + /// Contenido predeterminado para la página de error "*404 – Not Found*". /// /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema. fn error404(&self, page: &mut Page) -> Markup { - html! { div { h1 { (L10n::l("error404_notice").to_markup(page)) } } } + html! { div { h1 { (L10n::l("error404_notice").using(page)) } } } } } + +/// Se implementa automáticamente `ThemePage` para cualquier tema. +impl ThemePage for T {} diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index 4fcd7df..1a2e0fb 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -25,7 +25,7 @@ pub const REGION_CONTENT: &str = "content"; /// (p.ej., clases `region__{name}`). /// /// Se utiliza para declarar las regiones que componen una página en un tema (ver -/// [`declared_regions()`](crate::core::theme::Theme::declared_regions)). +/// [`page_regions()`](crate::core::theme::Theme::page_regions)). pub struct Region { key: &'static str, name: String, diff --git a/src/locale/en-US/welcome.ftl b/src/locale/en-US/welcome.ftl index 7d98f44..7b7d74d 100644 --- a/src/locale/en-US/welcome.ftl +++ b/src/locale/en-US/welcome.ftl @@ -3,7 +3,6 @@ welcome_extension_description = Displays a landing page when none is configured. welcome_page = Welcome Page welcome_title = Hello world! -welcome_aria = Say hello to your { $app } installation welcome_intro = Discover⚡{ $app } welcome_powered = A web solution powered by PageTop! diff --git a/src/locale/es-ES/welcome.ftl b/src/locale/es-ES/welcome.ftl index 8a38425..7823832 100644 --- a/src/locale/es-ES/welcome.ftl +++ b/src/locale/es-ES/welcome.ftl @@ -3,7 +3,6 @@ welcome_extension_description = Muestra una página de inicio predeterminada cua welcome_page = Página de Bienvenida welcome_title = ¡Hola mundo! -welcome_aria = Saluda a tu instalación { $app } welcome_intro = Descubre⚡{ $app } welcome_powered = Una solución web creada con PageTop! diff --git a/static/css/welcome.css b/static/css/intro.css similarity index 78% rename from static/css/welcome.css rename to static/css/intro.css index 4ce8046..5a5461e 100644 --- a/static/css/welcome.css +++ b/static/css/intro.css @@ -1,8 +1,8 @@ :root { - --bg-img: url('/img/welcome-header.jpg'); - --bg-img-set: image-set(url('/img/welcome-header.avif') type('image/avif'), url('/img/welcome-header.webp') type('image/webp'), var(--bg-img) type('image/jpeg')); - --bg-img-sm: url('/img/welcome-header-sm.jpg'); - --bg-img-sm-set: image-set(url('/img/welcome-header-sm.avif') type('image/avif'), url('/img/welcome-header-sm.webp') type('image/webp'), var(--bg-img-sm) type('image/jpeg')); + --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-red: #fecaca; @@ -28,9 +28,14 @@ body { font-weight: 300; color: var(--color); line-height: 1.6; + + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } -header, section { position: relative; text-align: center; @@ -50,19 +55,11 @@ a:hover:visited { text-decoration-color: var(--color-link); } -#content { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - /* - * Region header + * Header */ -.welcome-header { +.intro-header { display: flex; flex-direction: column-reverse; width: 100%; @@ -76,11 +73,11 @@ a:hover:visited { background-size: contain; background-repeat: no-repeat; } -.welcome-header__body { +.intro-header__body { padding: 0; background: none; } -.welcome-header__title { +.intro-header__title { margin: 0 0 0 1.5rem; text-align: left; display: flex; @@ -94,7 +91,7 @@ a:hover:visited { line-height: 110%; text-shadow: 0 0.125rem 0.1875rem rgba(0, 0, 0, 0.3); } -.welcome-header__title > span { +.intro-header__title > span { background: linear-gradient(180deg, #ddff95 30%, #ffb84b 100%); background-clip: text; -webkit-background-clip: text; @@ -105,44 +102,44 @@ a:hover:visited { line-height: 110%; text-shadow: none; } -.welcome-header__image { +.intro-header__image { display: flex; justify-content: flex-start; text-align: right; width: 100%; } -.welcome-header__monster { +.intro-header__monster { margin-right: 12rem; margin-top: 1rem; flex-shrink: 1; } @media (min-width: 64rem) { - .welcome-header { + .intro-header { background-image: var(--bg-img); background-image: var(--bg-img-set); } - .welcome-header__title { + .intro-header__title { padding: 1.2rem 2rem 2.6rem 2rem; } - .welcome-header__image { + .intro-header__image { justify-content: flex-end; } } /* - * Region content + * Content */ -.welcome-content { +.intro-content { height: auto; margin-top: 1.6rem; } -.welcome-content__body { +.intro-content__body { box-sizing: border-box; max-width: 80rem; } -.welcome-content__body:before, -.welcome-content__body:after { +.intro-content__body:before, +.intro-content__body:after { content: ''; position: absolute; left: 0; @@ -152,38 +149,37 @@ a:hover:visited { filter: blur(2.75rem); opacity: 0.8; inset: 11.75rem; - /*z-index: 0;*/ } -.welcome-content__body:before { +.intro-content__body:before { top: -1rem; } -.welcome-content__body:after { +.intro-content__body:after { bottom: -1rem; } @media (max-width: 48rem) { - .welcome-content__body { + .intro-content__body { margin-top: -9.8rem; } - .welcome-content__body:before, - .welcome-content__body:after { + .intro-content__body:before, + .intro-content__body:after { inset: unset; } } @media (min-width: 64rem) { - .welcome-content { + .intro-content { margin-top: 0; } - .welcome-content__body { + .intro-content__body { margin-top: -5.7rem; } } -.welcome-poweredby { +.intro-button { width: 100%; margin: 0 auto 3rem; z-index: 10; } -.welcome-poweredby__link { +.intro-button__link { background: #7f1d1d; background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0)); background-position: top left, center; @@ -196,7 +192,6 @@ a:hover:visited { font-size: 1.5rem; line-height: 1.3; text-decoration: none; - /*text-shadow: var(--shadow);*/ transition: transform 0.3s ease-in-out; position: relative; overflow: hidden; @@ -204,7 +199,7 @@ a:hover:visited { min-height: 7.6875rem; outline: none; } -.welcome-poweredby__link::before { +.intro-button__link::before { content: ''; position: absolute; top: -13.125rem; @@ -216,7 +211,7 @@ a:hover:visited { transition: transform 0.3s ease-in-out; z-index: 5; } -.welcome-poweredby__text { +.intro-button__text { display: flex; flex-direction: column; flex: 1; @@ -226,25 +221,24 @@ a:hover:visited { padding: 1rem 1.5rem; text-align: left; color: white; - /*text-shadow: 0 0.101125rem 0.2021875rem rgba(0, 0, 0, 0.25);*/ font-size: 1.65rem; font-style: normal; font-weight: 600; line-height: 130.023%; letter-spacing: 0.0075rem; } -.welcome-poweredby__text strong { +.intro-button__text strong { font-size: 2.625rem; font-weight: 600; line-height: 130.023%; letter-spacing: 0.013125rem; } -.welcome-poweredby__link span { +.intro-button__link span { position: absolute; display: block; pointer-events: none; } -.welcome-poweredby__link span:nth-child(1) { +.intro-button__link span:nth-child(1) { height: 8px; width: 100%; top: 0; @@ -264,7 +258,7 @@ a:hover:visited { transform: translateX(100%); } } -.welcome-poweredby__link span:nth-child(2) { +.intro-button__link span:nth-child(2) { width: 8px; height: 100%; top: 0; @@ -284,7 +278,7 @@ a:hover:visited { transform: translateY(100%); } } -.welcome-poweredby__link span:nth-child(3) { +.intro-button__link span:nth-child(3) { height: 8px; width: 100%; bottom: 0; @@ -304,22 +298,22 @@ a:hover:visited { transform: translateX(-100%); } } -.welcome-poweredby__link:hover span { +.intro-button__link:hover span { animation-play-state: paused; } @media (max-width: 48rem) { - .welcome-poweredby__link { + .intro-button__link { height: 6.25rem; min-width: auto; border-radius: 0; } - .welcome-poweredby__text { + .intro-button__text { display: inline; padding-top: .5rem; } } @media (min-width: 48rem) { - .welcome-poweredby { + .intro-button { position: absolute; top: 0; left: 50%; @@ -327,14 +321,13 @@ a:hover:visited { max-width: 29.375rem; margin-bottom: 0; } - .welcome-poweredby__link:hover { + .intro-button__link:hover { transition: all .5s; transform: rotate(-3deg) scale(1.1); - /*box-shadow: 0px 3px 5px rgba(0,0,0,.4);*/ } } -.welcome-text { +.intro-text { z-index: 1; width: 100%; display: flex; @@ -346,13 +339,16 @@ a:hover:visited { font-weight: 400; line-height: 1.5; margin-top: -6rem; - background: #fff; margin-bottom: 0; + background: #fff; position: relative; - padding: 6rem 1.063rem 0.75rem; + padding: 2.5rem 1.063rem 0.75rem; overflow: hidden; } -.welcome-text p { +.intro-button + .intro-text { + padding-top: 6rem; +} +.intro-text p { width: 100%; line-height: 150%; font-weight: 400; @@ -360,14 +356,16 @@ a:hover:visited { margin: 0 0 1.5rem; } @media (min-width: 48rem) { - .welcome-text { + .intro-text { font-size: 1.375rem; line-height: 2rem; + } + .intro-button + .intro-text { padding-top: 7rem; } } @media (min-width: 64rem) { - .welcome-text { + .intro-text { border-radius: 0.75rem; box-shadow: var(--shadow); max-width: 60rem; @@ -377,13 +375,13 @@ a:hover:visited { } } -.welcome-text__block { +.intro-text .block { position: relative; } -.welcome-text__block h2 { +.intro-text .block__title { margin: 1em 0 .8em; } -.welcome-text__block h2 span { +.intro-text .block__title span { display: inline-block; padding: 10px 30px 14px; margin: 0 0 0 20px; @@ -394,7 +392,7 @@ a:hover:visited { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.welcome-text__block h2:before { +.intro-text .block__title:before { content: ""; height: 5px; position: absolute; @@ -407,7 +405,7 @@ a:hover:visited { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.welcome-text__block h2:after { +.intro-text .block__title:after { content: ""; height: 70rem; position: absolute; @@ -420,15 +418,17 @@ a:hover:visited { } /* - * Region footer + * Footer */ -.region--footer { +.intro-footer { + width: 100%; background-color: black; color: var(--color-gray); + padding-bottom: 2rem; } -.welcome-footer { +.intro-footer__body { display: flex; justify-content: center; flex-direction: column; @@ -439,33 +439,33 @@ a:hover:visited { font-weight: 300; line-height: 100%; } -.welcome-footer a:visited { +.intro-footer__body a:visited { color: var(--color-gray); } -.welcome-footer__logo, -.welcome-footer__links { +.intro-footer__logo, +.intro-footer__links { display: flex; justify-content: center; width: 100%; } -.welcome-footer__logo { +.intro-footer__logo { max-height: 12.625rem; } -.welcome-footer__logo svg { +.intro-footer__logo svg { width: 100%; } -.welcome-footer__links { +.intro-footer__links { gap: 1.875rem; flex-wrap: wrap; margin-top: 2rem; } @media (max-width: 48rem) { - .welcome-footer__logo { + .intro-footer__logo { display: none; } } @media (max-width: 64rem) { - .welcome-footer { + .intro-footer__body { padding: 0 1rem 2rem; } } diff --git a/static/img/welcome-header-sm.avif b/static/img/intro-header-sm.avif similarity index 100% rename from static/img/welcome-header-sm.avif rename to static/img/intro-header-sm.avif diff --git a/static/img/welcome-header-sm.jpg b/static/img/intro-header-sm.jpg similarity index 100% rename from static/img/welcome-header-sm.jpg rename to static/img/intro-header-sm.jpg diff --git a/static/img/welcome-header-sm.webp b/static/img/intro-header-sm.webp similarity index 100% rename from static/img/welcome-header-sm.webp rename to static/img/intro-header-sm.webp diff --git a/static/img/welcome-header.avif b/static/img/intro-header.avif similarity index 100% rename from static/img/welcome-header.avif rename to static/img/intro-header.avif diff --git a/static/img/welcome-header.jpg b/static/img/intro-header.jpg similarity index 100% rename from static/img/welcome-header.jpg rename to static/img/intro-header.jpg diff --git a/static/img/welcome-header.webp b/static/img/intro-header.webp similarity index 100% rename from static/img/welcome-header.webp rename to static/img/intro-header.webp