🚧 [welcome] Crea página de bienvenida desde intro

- 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.
This commit is contained in:
Manuel Cillero 2025-09-04 01:53:51 +02:00
parent fe3bbcb131
commit 8274519405
17 changed files with 420 additions and 258 deletions

View file

@ -3,5 +3,8 @@
mod html; mod html;
pub use html::Html; pub use html::Html;
mod block;
pub use block::Block;
mod poweredby; mod poweredby;
pub use poweredby::PoweredBy; pub use poweredby::PoweredBy;

103
src/base/component/block.rs Normal file
View file

@ -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<String> {
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::<Block>(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<str>) -> 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<str>) -> 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
}
}

View file

@ -25,95 +25,26 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
let app = &global::SETTINGS.app.name; let app = &global::SETTINGS.app.name;
Page::new(Some(request)) Page::new(Some(request))
.with_title(L10n::l("welcome_page")) .with_theme("basic")
.with_theme("Basic") .with_layout("intro")
.with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/welcome.css"))) .with_title(L10n::l("welcome_title"))
.with_body_classes(ClassesOp::Add, "welcome") .with_description(L10n::l("welcome_intro").with_arg("app", app))
.with_component_in("header", Html::with(move |cx| html! { .add_component(Html::with(|cx| {
div class="welcome-header" { html! {
header class="welcome-header__body" { p { (L10n::l("welcome_text1").using(cx)) }
h1 p { (L10n::l("welcome_text2").using(cx)) }
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_component(Html::with(move |cx| html! { .add_component(
main class="welcome-content" { Block::new()
section class="welcome-content__body" { .with_title(L10n::l("welcome_about"))
div class="welcome-poweredby" { .add_component(Html::with(move |cx| {
a html! {
class="welcome-poweredby__link" p { (L10n::l("welcome_pagetop").using(cx)) }
href="https://pagetop.cillero.es" p { (L10n::l("welcome_issues1").using(cx)) }
target="_blank" p { (L10n::l("welcome_issues2").with_arg("app", app).using(cx)) }
rel="noreferrer"
{
span {} span {} span {}
div class="welcome-poweredby__text" {
(L10n::l("welcome_powered").to_markup(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() .render()
} }

View file

@ -12,16 +12,105 @@ impl Extension for Basic {
} }
impl Theme for Basic { impl Theme for Basic {
fn render_page_body(&self, page: &mut Page) -> Markup {
match page.layout() {
"intro" => render_intro(page),
_ => <Self as ThemePage>::render_body(self, page, self.page_regions()),
}
}
fn after_render_page_body(&self, page: &mut Page) { 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( page.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/css/normalize.css") StyleSheet::from("/css/normalize.css")
.with_version("8.0.1") .with_version("8.0.1")
.with_weight(-99), .with_weight(-99),
)) ))
.alter_assets(AssetsOp::AddStyleSheet( .alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/css/basic.css") StyleSheet::from(styles)
.with_version(env!("CARGO_PKG_VERSION")) .with_version(env!("CARGO_PKG_VERSION"))
.with_weight(-99), .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)) }
}
}
}
}
}
}

View file

@ -29,14 +29,14 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
TypeInfo::ShortName.of::<Self>() TypeInfo::ShortName.of::<Self>()
} }
/// Devuelve una descripción opcional del componente. /// Devuelve una descripción del componente, si existe.
/// ///
/// Por defecto, no se proporciona ninguna descripción (`None`). /// Por defecto, no se proporciona ninguna descripción (`None`).
fn description(&self) -> Option<String> { fn description(&self) -> Option<String> {
None 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 /// Este identificador puede usarse para referenciar el componente en el HTML. Por defecto, no
/// tiene ningún identificador (`None`). /// tiene ningún identificador (`None`).
@ -51,7 +51,7 @@ pub trait Component: AnyInfo + ComponentRender + Send + Sync {
#[allow(unused_variables)] #[allow(unused_variables)]
fn setup_before_prepare(&mut self, cx: &mut Context) {} 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 /// 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 /// durante el proceso de construcción del documento. Puede sobrescribirse para generar

View file

@ -15,10 +15,10 @@
//! [`Theme`]. //! [`Theme`].
mod definition; mod definition;
pub use definition::{Theme, ThemeRef}; pub use definition::{Theme, ThemePage, ThemeRef};
mod regions; mod regions;
pub(crate) use regions::ChildrenInRegions; pub(crate) use regions::{ChildrenInRegions, REGION_CONTENT};
pub use regions::{InRegion, Region, REGION_CONTENT}; pub use regions::{InRegion, Region};
pub(crate) mod all; pub(crate) mod all;

View file

@ -7,88 +7,34 @@ use crate::response::page::Page;
use std::sync::LazyLock; 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 /// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
/// estáticas globales que implementan [`Theme`], pero también [`Extension`]. /// implementen [`Theme`] y, a su vez, [`Extension`].
pub type ThemeRef = &'static dyn Theme; 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 /// Contiene las implementaciones base de las **secciones** `<head>` y `<body>`. Se implementa
/// único obligatorio será [`theme()`](Extension::theme). /// automáticamente para cualquier tipo que implemente [`Theme`], por lo que normalmente no requiere
/// implementación explícita.
/// ///
/// ```rust /// Si un tema **sobrescribe** [`render_page_head()`](Theme::render_page_head) o
/// use pagetop::prelude::*; /// [`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; /// - `<Self as ThemePage>::render_body(self, page, self.page_regions())`
/// /// - `<Self as ThemePage>::render_head(self, page)`
/// impl Extension for MyTheme { pub trait ThemePage {
/// fn name(&self) -> L10n {
/// L10n::n("My theme")
/// }
///
/// fn description(&self) -> L10n {
/// L10n::n("A personal theme")
/// }
///
/// fn theme(&self) -> Option<ThemeRef> {
/// 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")),
]
});
&REGIONS[..]
}
/// Acciones específicas del tema antes de renderizar el `<body>` 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 `<body>` de la página. /// Renderiza el contenido del `<body>` de la página.
/// ///
/// Por defecto, recorre [`declared_regions()`](Self::declared_regions) **en el orden que se han /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, genera un
/// declarado** y, para cada región con contenido, genera un contenedor con `role="region"` y /// contenedor con `role="region"` y un `aria-label` localizado. Se asume que cada identificador
/// `aria-label` localizado. /// de región es **único** dentro de la página.
fn render_page_body(&self, page: &mut Page) -> Markup { fn render_body(&self, page: &mut Page, regions: &[(Region, L10n)]) -> Markup {
html! { html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] { 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()); @let output = page.render_region(region.key());
@if !output.is_empty() { @if !output.is_empty() {
@let region_name = region.name(); @let region_name = region.name();
@ -96,7 +42,7 @@ pub trait Theme: Extension + Send + Sync {
id=(region_name) id=(region_name)
class={ "region region--" (region_name) } class={ "region region--" (region_name) }
role="region" role="region"
aria-label=[region_label.using(page)] aria-label=[region_label.lookup(page)]
{ {
(output) (output)
} }
@ -106,17 +52,12 @@ pub trait Theme: Extension + Send + Sync {
} }
} }
/// Acciones específicas del tema después de renderizar el `<body>` 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 `<head>` de la página. /// Renderiza el contenido del `<head>` de la página.
/// ///
/// Por defecto, genera las etiquetas básicas (`charset`, `title`, `description`, `viewport`, /// Por defecto incluye las etiquetas básicas (`charset`, `title`, `description`, `viewport`,
/// `X-UA-Compatible`), los metadatos y propiedades de la página y los recursos (CSS/JS). /// `X-UA-Compatible`), los metadatos (`name/content`) y propiedades (`property/content`),
fn render_page_head(&self, page: &mut Page) -> Markup { /// 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"; let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! { html! {
head { 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<ThemeRef> {
/// 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")),
]
});
&REGIONS[..]
}
/// Acciones específicas del tema antes de renderizar el `<body>` 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 `<body>` de la página.
///
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
/// `<Self as ThemePage>::render_body(self, page, self.page_regions())`.
#[inline]
fn render_page_body(&self, page: &mut Page) -> Markup {
<Self as ThemePage>::render_body(self, page, self.page_regions())
}
/// Acciones específicas del tema después de renderizar el `<body>` 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 `<head>` de la página.
///
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
/// `<Self as ThemePage>::render_head(self, page)`.
#[inline]
fn render_page_head(&self, page: &mut Page) -> Markup {
<Self as ThemePage>::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. /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema.
fn error403(&self, page: &mut Page) -> Markup { 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. /// Se puede sobrescribir este método para personalizar y adaptar este contenido al tema.
fn error404(&self, page: &mut Page) -> Markup { 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<T: Theme> ThemePage for T {}

View file

@ -25,7 +25,7 @@ pub const REGION_CONTENT: &str = "content";
/// (p.ej., clases `region__{name}`). /// (p.ej., clases `region__{name}`).
/// ///
/// Se utiliza para declarar las regiones que componen una página en un tema (ver /// 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 { pub struct Region {
key: &'static str, key: &'static str,
name: String, name: String,

View file

@ -3,7 +3,6 @@ welcome_extension_description = Displays a landing page when none is configured.
welcome_page = Welcome Page welcome_page = Welcome Page
welcome_title = Hello world! welcome_title = Hello world!
welcome_aria = Say hello to your { $app } installation
welcome_intro = Discover⚡{ $app } welcome_intro = Discover⚡{ $app }
welcome_powered = A web solution powered by <strong>PageTop!</strong> welcome_powered = A web solution powered by <strong>PageTop!</strong>

View file

@ -3,7 +3,6 @@ welcome_extension_description = Muestra una página de inicio predeterminada cua
welcome_page = Página de Bienvenida welcome_page = Página de Bienvenida
welcome_title = ¡Hola mundo! welcome_title = ¡Hola mundo!
welcome_aria = Saluda a tu instalación { $app }
welcome_intro = Descubre⚡{ $app } welcome_intro = Descubre⚡{ $app }
welcome_powered = Una solución web creada con <strong>PageTop!</strong> welcome_powered = Una solución web creada con <strong>PageTop!</strong>

View file

@ -1,8 +1,8 @@
:root { :root {
--bg-img: url('/img/welcome-header.jpg'); --bg-img: url('/img/intro-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-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/welcome-header-sm.jpg'); --bg-img-sm: url('/img/intro-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-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; --bg-color: #8c5919;
--color: #1a202c; --color: #1a202c;
--color-red: #fecaca; --color-red: #fecaca;
@ -28,9 +28,14 @@ body {
font-weight: 300; font-weight: 300;
color: var(--color); color: var(--color);
line-height: 1.6; line-height: 1.6;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
} }
header,
section { section {
position: relative; position: relative;
text-align: center; text-align: center;
@ -50,19 +55,11 @@ a:hover:visited {
text-decoration-color: var(--color-link); 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; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
width: 100%; width: 100%;
@ -76,11 +73,11 @@ a:hover:visited {
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.welcome-header__body { .intro-header__body {
padding: 0; padding: 0;
background: none; background: none;
} }
.welcome-header__title { .intro-header__title {
margin: 0 0 0 1.5rem; margin: 0 0 0 1.5rem;
text-align: left; text-align: left;
display: flex; display: flex;
@ -94,7 +91,7 @@ a:hover:visited {
line-height: 110%; line-height: 110%;
text-shadow: 0 0.125rem 0.1875rem rgba(0, 0, 0, 0.3); 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: linear-gradient(180deg, #ddff95 30%, #ffb84b 100%);
background-clip: text; background-clip: text;
-webkit-background-clip: text; -webkit-background-clip: text;
@ -105,44 +102,44 @@ a:hover:visited {
line-height: 110%; line-height: 110%;
text-shadow: none; text-shadow: none;
} }
.welcome-header__image { .intro-header__image {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
text-align: right; text-align: right;
width: 100%; width: 100%;
} }
.welcome-header__monster { .intro-header__monster {
margin-right: 12rem; margin-right: 12rem;
margin-top: 1rem; margin-top: 1rem;
flex-shrink: 1; flex-shrink: 1;
} }
@media (min-width: 64rem) { @media (min-width: 64rem) {
.welcome-header { .intro-header {
background-image: var(--bg-img); background-image: var(--bg-img);
background-image: var(--bg-img-set); background-image: var(--bg-img-set);
} }
.welcome-header__title { .intro-header__title {
padding: 1.2rem 2rem 2.6rem 2rem; padding: 1.2rem 2rem 2.6rem 2rem;
} }
.welcome-header__image { .intro-header__image {
justify-content: flex-end; justify-content: flex-end;
} }
} }
/* /*
* Region content * Content
*/ */
.welcome-content { .intro-content {
height: auto; height: auto;
margin-top: 1.6rem; margin-top: 1.6rem;
} }
.welcome-content__body { .intro-content__body {
box-sizing: border-box; box-sizing: border-box;
max-width: 80rem; max-width: 80rem;
} }
.welcome-content__body:before, .intro-content__body:before,
.welcome-content__body:after { .intro-content__body:after {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; left: 0;
@ -152,38 +149,37 @@ a:hover:visited {
filter: blur(2.75rem); filter: blur(2.75rem);
opacity: 0.8; opacity: 0.8;
inset: 11.75rem; inset: 11.75rem;
/*z-index: 0;*/
} }
.welcome-content__body:before { .intro-content__body:before {
top: -1rem; top: -1rem;
} }
.welcome-content__body:after { .intro-content__body:after {
bottom: -1rem; bottom: -1rem;
} }
@media (max-width: 48rem) { @media (max-width: 48rem) {
.welcome-content__body { .intro-content__body {
margin-top: -9.8rem; margin-top: -9.8rem;
} }
.welcome-content__body:before, .intro-content__body:before,
.welcome-content__body:after { .intro-content__body:after {
inset: unset; inset: unset;
} }
} }
@media (min-width: 64rem) { @media (min-width: 64rem) {
.welcome-content { .intro-content {
margin-top: 0; margin-top: 0;
} }
.welcome-content__body { .intro-content__body {
margin-top: -5.7rem; margin-top: -5.7rem;
} }
} }
.welcome-poweredby { .intro-button {
width: 100%; width: 100%;
margin: 0 auto 3rem; margin: 0 auto 3rem;
z-index: 10; z-index: 10;
} }
.welcome-poweredby__link { .intro-button__link {
background: #7f1d1d; background: #7f1d1d;
background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0)); background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0));
background-position: top left, center; background-position: top left, center;
@ -196,7 +192,6 @@ a:hover:visited {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.3; line-height: 1.3;
text-decoration: none; text-decoration: none;
/*text-shadow: var(--shadow);*/
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -204,7 +199,7 @@ a:hover:visited {
min-height: 7.6875rem; min-height: 7.6875rem;
outline: none; outline: none;
} }
.welcome-poweredby__link::before { .intro-button__link::before {
content: ''; content: '';
position: absolute; position: absolute;
top: -13.125rem; top: -13.125rem;
@ -216,7 +211,7 @@ a:hover:visited {
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
z-index: 5; z-index: 5;
} }
.welcome-poweredby__text { .intro-button__text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
@ -226,25 +221,24 @@ a:hover:visited {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
text-align: left; text-align: left;
color: white; color: white;
/*text-shadow: 0 0.101125rem 0.2021875rem rgba(0, 0, 0, 0.25);*/
font-size: 1.65rem; font-size: 1.65rem;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
line-height: 130.023%; line-height: 130.023%;
letter-spacing: 0.0075rem; letter-spacing: 0.0075rem;
} }
.welcome-poweredby__text strong { .intro-button__text strong {
font-size: 2.625rem; font-size: 2.625rem;
font-weight: 600; font-weight: 600;
line-height: 130.023%; line-height: 130.023%;
letter-spacing: 0.013125rem; letter-spacing: 0.013125rem;
} }
.welcome-poweredby__link span { .intro-button__link span {
position: absolute; position: absolute;
display: block; display: block;
pointer-events: none; pointer-events: none;
} }
.welcome-poweredby__link span:nth-child(1) { .intro-button__link span:nth-child(1) {
height: 8px; height: 8px;
width: 100%; width: 100%;
top: 0; top: 0;
@ -264,7 +258,7 @@ a:hover:visited {
transform: translateX(100%); transform: translateX(100%);
} }
} }
.welcome-poweredby__link span:nth-child(2) { .intro-button__link span:nth-child(2) {
width: 8px; width: 8px;
height: 100%; height: 100%;
top: 0; top: 0;
@ -284,7 +278,7 @@ a:hover:visited {
transform: translateY(100%); transform: translateY(100%);
} }
} }
.welcome-poweredby__link span:nth-child(3) { .intro-button__link span:nth-child(3) {
height: 8px; height: 8px;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
@ -304,22 +298,22 @@ a:hover:visited {
transform: translateX(-100%); transform: translateX(-100%);
} }
} }
.welcome-poweredby__link:hover span { .intro-button__link:hover span {
animation-play-state: paused; animation-play-state: paused;
} }
@media (max-width: 48rem) { @media (max-width: 48rem) {
.welcome-poweredby__link { .intro-button__link {
height: 6.25rem; height: 6.25rem;
min-width: auto; min-width: auto;
border-radius: 0; border-radius: 0;
} }
.welcome-poweredby__text { .intro-button__text {
display: inline; display: inline;
padding-top: .5rem; padding-top: .5rem;
} }
} }
@media (min-width: 48rem) { @media (min-width: 48rem) {
.welcome-poweredby { .intro-button {
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
@ -327,14 +321,13 @@ a:hover:visited {
max-width: 29.375rem; max-width: 29.375rem;
margin-bottom: 0; margin-bottom: 0;
} }
.welcome-poweredby__link:hover { .intro-button__link:hover {
transition: all .5s; transition: all .5s;
transform: rotate(-3deg) scale(1.1); transform: rotate(-3deg) scale(1.1);
/*box-shadow: 0px 3px 5px rgba(0,0,0,.4);*/
} }
} }
.welcome-text { .intro-text {
z-index: 1; z-index: 1;
width: 100%; width: 100%;
display: flex; display: flex;
@ -346,13 +339,16 @@ a:hover:visited {
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
margin-top: -6rem; margin-top: -6rem;
background: #fff;
margin-bottom: 0; margin-bottom: 0;
background: #fff;
position: relative; position: relative;
padding: 6rem 1.063rem 0.75rem; padding: 2.5rem 1.063rem 0.75rem;
overflow: hidden; overflow: hidden;
} }
.welcome-text p { .intro-button + .intro-text {
padding-top: 6rem;
}
.intro-text p {
width: 100%; width: 100%;
line-height: 150%; line-height: 150%;
font-weight: 400; font-weight: 400;
@ -360,14 +356,16 @@ a:hover:visited {
margin: 0 0 1.5rem; margin: 0 0 1.5rem;
} }
@media (min-width: 48rem) { @media (min-width: 48rem) {
.welcome-text { .intro-text {
font-size: 1.375rem; font-size: 1.375rem;
line-height: 2rem; line-height: 2rem;
}
.intro-button + .intro-text {
padding-top: 7rem; padding-top: 7rem;
} }
} }
@media (min-width: 64rem) { @media (min-width: 64rem) {
.welcome-text { .intro-text {
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: var(--shadow); box-shadow: var(--shadow);
max-width: 60rem; max-width: 60rem;
@ -377,13 +375,13 @@ a:hover:visited {
} }
} }
.welcome-text__block { .intro-text .block {
position: relative; position: relative;
} }
.welcome-text__block h2 { .intro-text .block__title {
margin: 1em 0 .8em; margin: 1em 0 .8em;
} }
.welcome-text__block h2 span { .intro-text .block__title span {
display: inline-block; display: inline-block;
padding: 10px 30px 14px; padding: 10px 30px 14px;
margin: 0 0 0 20px; margin: 0 0 0 20px;
@ -394,7 +392,7 @@ a:hover:visited {
border-color: orangered; border-color: orangered;
transform: rotate(-3deg) translateY(-25%); transform: rotate(-3deg) translateY(-25%);
} }
.welcome-text__block h2:before { .intro-text .block__title:before {
content: ""; content: "";
height: 5px; height: 5px;
position: absolute; position: absolute;
@ -407,7 +405,7 @@ a:hover:visited {
transform: rotate(2deg) translateY(-50%); transform: rotate(2deg) translateY(-50%);
transform-origin: top left; transform-origin: top left;
} }
.welcome-text__block h2:after { .intro-text .block__title:after {
content: ""; content: "";
height: 70rem; height: 70rem;
position: absolute; position: absolute;
@ -420,15 +418,17 @@ a:hover:visited {
} }
/* /*
* Region footer * Footer
*/ */
.region--footer { .intro-footer {
width: 100%;
background-color: black; background-color: black;
color: var(--color-gray); color: var(--color-gray);
padding-bottom: 2rem;
} }
.welcome-footer { .intro-footer__body {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@ -439,33 +439,33 @@ a:hover:visited {
font-weight: 300; font-weight: 300;
line-height: 100%; line-height: 100%;
} }
.welcome-footer a:visited { .intro-footer__body a:visited {
color: var(--color-gray); color: var(--color-gray);
} }
.welcome-footer__logo, .intro-footer__logo,
.welcome-footer__links { .intro-footer__links {
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.welcome-footer__logo { .intro-footer__logo {
max-height: 12.625rem; max-height: 12.625rem;
} }
.welcome-footer__logo svg { .intro-footer__logo svg {
width: 100%; width: 100%;
} }
.welcome-footer__links { .intro-footer__links {
gap: 1.875rem; gap: 1.875rem;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 2rem; margin-top: 2rem;
} }
@media (max-width: 48rem) { @media (max-width: 48rem) {
.welcome-footer__logo { .intro-footer__logo {
display: none; display: none;
} }
} }
@media (max-width: 64rem) { @media (max-width: 64rem) {
.welcome-footer { .intro-footer__body {
padding: 0 1rem 2rem; padding: 0 1rem 2rem;
} }
} }

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Before After
Before After