♻️ Refactoriza página de bienvenida y tema Basic

- Actualiza `Welcome` para usar el nuevo componente `Intro`.
- Simplifica el tema `Basic` apoyándose en la lógica de `Theme`.
- Predefine los *assets* básicos como recursos de `Theme`.
- Refactoriza archivos de localicación para reflejar los cambios de los
componentes.
This commit is contained in:
Manuel Cillero 2025-10-12 09:15:50 +02:00
parent d0beb8ef40
commit ebf1828ea3
12 changed files with 455 additions and 296 deletions

View file

@ -108,7 +108,7 @@ impl Extension for Aliner {
impl Theme for Aliner {
fn before_render_page_body(&self, page: &mut Page) {
page.alter_param("include_basic_css", true)
page.alter_param("include_basic_assets", true)
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/aliner/css/styles.css")
.with_version(env!("CARGO_PKG_VERSION"))

View file

@ -54,6 +54,9 @@ pub use html::Html;
mod block;
pub use block::Block;
mod intro;
pub use intro::{Intro, IntroOpening};
mod poweredby;
pub use poweredby::PoweredBy;

343
src/base/component/intro.rs Normal file
View file

@ -0,0 +1,343 @@
use crate::prelude::*;
/// Tipo de apertura que se mostrará en la introducción del componente [`Intro`].
///
/// Permite elegir entre una apertura con textos predefinidos sobre PageTop (como hace la página de
/// bienvenida [`Welcome`](crate::base::extension::Welcome)) o una introducción completamente
/// personalizada.
#[derive(AutoDefault, Copy, Clone, Debug, Eq, PartialEq)]
pub enum IntroOpening {
/// Modo por defecto. Muestra una introducción estándar de PageTop e incluye automáticamente
/// *badges* con información de la última versión liberada, fecha del último lanzamiento y
/// licencia de uso.
#[default]
PageTop,
/// Modo totalmente personalizado. No añade *badges* ni textos predefinidos. Usa la imagen de
/// PageTop pero el contenido lo define el propio desarrollador.
Custom,
}
/// Componente para presentar PageTop (como [`Welcome`](crate::base::extension::Welcome)), o mostrar
/// introducciones.
///
/// Usa la imagen de PageTop para presentar contenidos con:
///
/// - Una **imagen decorativa** (el *monster* de PageTop) antecediendo al contenido.
/// - Una vista destacada con **título + eslogan**.
/// - Un **botón opcional** de llamada a la acción con texto y enlace configurables.
/// - El **área de textos** con *badges* predefinidos (en modo [`IntroOpening::PageTop`]) y bloques
/// ([`Block`](crate::base::component::Block)) para crear párrafos vistosos de texto. Aunque
/// admite todo tipo de componentes.
///
/// ### Ejemplos
///
/// **Intro mínima por defecto**
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default();
/// ```
///
/// **Título, eslogan y botón personalizados**
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default()
/// .with_title(L10n::l("intro_custom_title"))
/// .with_slogan(L10n::l("intro_custom_slogan"))
/// .with_button(Some((
/// L10n::l("intro_learn_more"),
/// |_| "/learn-more"
/// )));
/// ```
///
/// **Sin botón + modo *Custom* (sin *badges* predefinidos)**
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default()
/// .with_button(None::<(L10n, FnPathByContext)>)
/// .with_opening(IntroOpening::Custom);
/// ```
///
/// **Añadir contenidos hijo**
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default()
/// .add_component(
/// Block::new()
/// .with_title(L10n::l("intro_custom_block_title"))
/// .add_component(Html::with(move |cx| {
/// html! {
/// p { (L10n::l("intro_custom_paragraph_1").using(cx)) }
/// p { (L10n::l("intro_custom_paragraph_2").using(cx)) }
/// }
/// })),
/// );
/// ```
#[rustfmt::skip]
pub struct Intro {
title : L10n,
slogan : L10n,
button : Option<(L10n, FnPathByContext)>,
opening : IntroOpening,
children: Children,
}
impl Default for Intro {
#[rustfmt::skip]
fn default() -> Self {
Intro {
title : L10n::l("intro_default_title"),
slogan : L10n::l("intro_default_slogan").with_arg("app", &global::SETTINGS.app.name),
button : Some((L10n::l("intro_default_button"), |_| "https://pagetop.cillero.es")),
opening : IntroOpening::default(),
children: Children::default(),
}
}
}
impl Component for Intro {
fn new() -> Self {
Intro::default()
}
fn setup_before_prepare(&mut self, cx: &mut Context) {
cx.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/intro.css").with_version(env!("CARGO_PKG_VERSION")),
));
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
if self.opening() == IntroOpening::PageTop {
cx.alter_assets(ContextOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
util::indoc!(r#"
try {
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
const data = await resp.json();
const date = new Date(data.versions[0].created_at);
const formatted = date.toLocaleDateString("LANGID", { year: "numeric", month: "2-digit", day: "2-digit" });
document.getElementById("intro-release").src = `https://img.shields.io/badge/Release%20date-${encodeURIComponent(formatted)}-blue?label=LABEL&style=for-the-badge`;
document.getElementById("intro-badges").style.display = "block";
} catch (e) {
console.error("Failed to fetch release date from crates.io:", e);
}
"#)
.replace("LANGID", cx.langid().to_string().as_str())
.replace("LABEL", L10n::l("intro_release_label").using(cx).as_str())
)));
}
PrepareMarkup::With(html! {
div class="intro" {
div class="intro-header" {
section class="intro-header__body" {
h1 class="intro-header__title" {
span { (self.title().using(cx)) }
(self.slogan().using(cx))
}
}
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";
}
}
}
}
div class="intro-content" {
section class="intro-content__body" {
div class="intro-text" {
@if let Some((txt, lnk)) = self.button() {
div class="intro-button" {
a
class="intro-button__link"
href=((lnk)(cx))
target="_blank"
rel="noreferrer"
{
span {} span {} span {}
div class="intro-button__text" {
(txt.using(cx))
}
}
}
}
div class="intro-text__children" {
@if self.opening() == IntroOpening::PageTop {
p { (L10n::l("intro_text1").using(cx)) }
div id="intro-badges" {
img
src="https://img.shields.io/crates/v/pagetop.svg?label=PageTop&style=for-the-badge"
alt=[L10n::l("intro_pagetop_label").lookup(cx)] {} (" ")
img
id="intro-release"
alt=[L10n::l("intro_release_label").lookup(cx)] {} (" ")
img
src=(format!(
"https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label={}&style=for-the-badge",
L10n::l("intro_license_label").lookup(cx).unwrap_or_default()
))
alt=[L10n::l("intro_license_label").lookup(cx)] {}
}
p { (L10n::l("intro_text2").using(cx)) }
}
(self.children().render(cx))
}
}
}
}
div 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(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="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(cx)) }
em { (L10n::l("intro_have_fun").using(cx)) }
}
}
}
}
})
}
}
impl Intro {
// **< Intro BUILDER >**************************************************************************
/// Establece el título de entrada.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default().with_title(L10n::n("Título de entrada"));
/// ```
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title = title;
self
}
/// Establece el eslogan de entrada (línea secundaria del título).
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default().with_slogan(L10n::n("Un eslogan para la entrada"));
/// ```
#[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan = slogan;
self
}
/// Configura el botón opcional de llamada a la acción.
///
/// - Usa `Some((texto, closure_url))` para mostrarlo, donde [`FnPathByContext`] recibe el
/// [`Context`] y devuelve la ruta o URL final al pulsar el botón.
/// - Usa `None` para ocultarlo.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// // Define un botón con texto y una URL fija.
/// let intro = Intro::default().with_button(Some((L10n::n("Pulsa este botón"), |_| "/start")));
/// // Descarta el botón de la intro.
/// let intro_no_button = Intro::default().with_button(None);
/// ```
#[builder_fn]
pub fn with_button(mut self, button: Option<(L10n, FnPathByContext)>) -> Self {
self.button = button;
self
}
/// Selecciona el tipo de apertura: [`IntroOpening::PageTop`] (por defecto) o
/// [`IntroOpening::Custom`].
///
/// - `PageTop`: añade *badges* automáticos y una presentación de lo que es PageTop.
/// - `Custom`: introducción en blanco para añadir cualquier contenido.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// let intro = Intro::default().with_opening(IntroOpening::Custom);
/// ```
#[builder_fn]
pub fn with_opening(mut self, opening: IntroOpening) -> Self {
self.opening = opening;
self
}
/// Añade un nuevo componente hijo a la intro.
///
/// Si es un bloque ([`Block`]) aplica estilos específicos para destacarlo.
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 [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// **< Intro GETTERS >**************************************************************************
/// Devuelve el título de entrada.
pub fn title(&self) -> &L10n {
&self.title
}
/// Devuelve el eslogan de la entrada.
pub fn slogan(&self) -> &L10n {
&self.slogan
}
/// Devuelve el botón de llamada a la acción, si existe.
pub fn button(&self) -> Option<(&L10n, &FnPathByContext)> {
self.button.as_ref().map(|(txt, lnk)| (txt, lnk))
}
/// Devuelve el modo de apertura configurado.
pub fn opening(&self) -> IntroOpening {
self.opening
}
/// Devuelve la lista de hijos (`children`) de la intro.
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -26,30 +26,29 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme("Basic")
.with_layout("PageTopIntro")
.with_title(L10n::l("welcome_title"))
.with_description(L10n::l("welcome_intro").with_arg("app", app))
.with_param("intro_button_txt", L10n::l("welcome_powered"))
.with_param("intro_button_lnk", "https://pagetop.cillero.es".to_string())
.add_component(
Block::new()
.with_title(L10n::l("welcome_status_title"))
.add_component(Html::with(move |cx| {
html! {
p { (L10n::l("welcome_status_1").using(cx)) }
p { (L10n::l("welcome_status_2").using(cx)) }
}
})),
)
.add_component(
Block::new()
.with_title(L10n::l("welcome_support_title"))
.add_component(Html::with(move |cx| {
html! {
p { (L10n::l("welcome_support_1").using(cx)) }
p { (L10n::l("welcome_support_2").with_arg("app", app).using(cx)) }
}
})),
Intro::new()
.add_component(
Block::new()
.with_title(L10n::l("welcome_status_title"))
.add_component(Html::with(move |cx| {
html! {
p { (L10n::l("welcome_status_1").using(cx)) }
p { (L10n::l("welcome_status_2").using(cx)) }
}
})),
)
.add_component(
Block::new()
.with_title(L10n::l("welcome_support_title"))
.add_component(Html::with(move |cx| {
html! {
p { (L10n::l("welcome_support_1").using(cx)) }
p { (L10n::l("welcome_support_2").with_arg("app", app).using(cx)) }
}
})),
),
)
.render()
}

View file

@ -4,33 +4,7 @@ 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*):
///
/// - **Composición predeterminada**
/// - Renderizado genérico con
/// [`ThemePage::render_body()`](crate::core::theme::ThemePage::render_body) usando las regiones
/// predefinidas en [`page_regions()`](crate::core::theme::Theme::page_regions).
///
/// - **`Intro`**
/// - Página de entrada con cabecera visual, título y descripción y un botón opcional de llamada a
/// la acción. Ideal para una página de inicio o bienvenida en el contexto de PageTop.
/// - **Regiones:** `content` (se renderiza dentro de `.intro-content__body`).
/// - **Parámetros:**
/// - `intro_button_txt` (`L10n`) Texto del botón.
/// - `intro_button_lnk` (`Option<String>`) URL del botón; si no se indica, el botón no se
/// muestra.
///
/// - **`PageTopIntro`**
/// - Variante de `Intro` con textos predefinidos sobre PageTop al inicio del contenido. Añade una
/// banda de *badges* con la versión de [PageTop en crates.io](https://crates.io/crates/pagetop)
/// más la fecha de la última versión publicada y la licencia de uso.
/// - **Regiones:** `content` (igual que `Intro`).
/// - **Parámetros:** los mismos que `Intro`.
///
/// **Nota:** si no se especifica `layout` o el valor no coincide con ninguno de los anteriores, se
/// aplica la composición predeterminada.
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
pub struct Basic;
impl Extension for Basic {
@ -40,180 +14,7 @@ impl Extension for Basic {
}
impl Theme for Basic {
fn render_page_body(&self, page: &mut Page) -> Markup {
match page.layout() {
"Intro" => render_intro(page),
"PageTopIntro" => render_pagetop_intro(page),
_ => <Self as ThemePage>::render_body(self, page, self.page_regions()),
}
}
fn after_render_page_body(&self, page: &mut Page) {
let pkg_version = env!("CARGO_PKG_VERSION");
page.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/normalize.css")
.with_version("8.0.1")
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/root.css")
.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)
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/menu.css")
.with_version(pkg_version)
.with_weight(-99),
))
.alter_assets(ContextOp::AddJavaScript(
JavaScript::defer("/js/menu.js")
.with_version(pkg_version)
.with_weight(-99),
));
fn before_render_page_body(&self, page: &mut Page) {
page.alter_param("include_basic_assets", true);
}
}
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)
}
}
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" {
@if intro_button_lnk.is_some() {
div class="intro-button" {
a
class="intro-button__link"
href=[intro_button_lnk]
target="_blank"
rel="noreferrer"
{
span {} span {} span {}
div class="intro-button__text" {
(intro_button_txt.using(page))
}
}
}
}
(c)
}
}
}
footer class="intro-footer" {
section class="intro-footer__body" {
div class="intro-footer__logo" {
svg
viewBox="0 0 1614 1614"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label=[L10n::l("pagetop_logo").lookup(page)]
preserveAspectRatio="xMidYMid slice"
focusable="false"
{
path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
path fill="rgb(255,255,255)" d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
}
}
div class="intro-footer__links" {
a href="https://crates.io/crates/pagetop" target="_blank" rel="noreferrer" { ("Crates.io") }
a href="https://docs.rs/pagetop" target="_blank" rel="noreferrer" { ("Docs.rs") }
a href="https://git.cillero.es/manuelcillero/pagetop" target="_blank" rel="noreferrer" { (L10n::l("intro_code").using(page)) }
em { (L10n::l("intro_have_fun").using(page)) }
}
}
(f)
}
}
}
fn render_pagetop_intro(page: &mut Page) -> Markup {
page.alter_assets(ContextOp::AddJavaScript(JavaScript::on_load_async("intro-js", |cx|
util::indoc!(r#"
try {
const resp = await fetch("https://crates.io/api/v1/crates/pagetop");
const data = await resp.json();
const date = new Date(data.versions[0].created_at);
const formatted = date.toLocaleDateString("LANGID", { year: "numeric", month: "2-digit", day: "2-digit" });
document.getElementById("intro-release").src = `https://img.shields.io/badge/Release%20date-${encodeURIComponent(formatted)}-blue?label=LABEL&style=for-the-badge`;
document.getElementById("intro-badges").style.display = "block";
} catch (e) {
console.error("Failed to fetch release date from crates.io:", e);
}
"#)
.replace("LANGID", cx.langid().to_string().as_str())
.replace("LABEL", L10n::l("intro_release_label").using(cx).as_str())
.to_string(),
)))
.alter_child_in("content", ChildOp::Prepend(Child::with(Html::with(|cx| html! {
p { (L10n::l("intro_text1").using(cx)) }
div id="intro-badges" style="display: none; margin-bottom: 1.1rem;" {
img
src="https://img.shields.io/crates/v/pagetop.svg?label=PageTop&style=for-the-badge"
alt=[L10n::l("intro_pagetop_label").lookup(cx)] {} (" ")
img
id="intro-release"
alt=[L10n::l("intro_release_label").lookup(cx)] {} (" ")
img
src=(format!(
"https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label={}&style=for-the-badge",
L10n::l("intro_license_label").lookup(cx).unwrap_or_default()
))
alt=[L10n::l("intro_license_label").lookup(cx)] {}
}
p { (L10n::l("intro_text2").using(cx)) }
}))));
render_intro(page)
}

View file

@ -1,6 +1,7 @@
use crate::core::component::{ContextOp, Contextual};
use crate::core::extension::Extension;
use crate::core::theme::{Region, RegionRef, REGION_CONTENT};
use crate::html::{html, Markup};
use crate::html::{html, Markup, StyleSheet};
use crate::locale::L10n;
use crate::response::page::Page;
use crate::{global, join};
@ -241,7 +242,7 @@ pub trait Theme: Extension + ThemePage + Send + Sync {
/// Renderiza el contenido del `<body>` de la página.
///
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
/// Si se sobrescribe este método, se puede volver al renderizado base con:
/// `<Self as ThemePage>::render_body(self, page, self.page_regions())`.
#[inline]
fn render_page_body(&self, page: &mut Page) -> Markup {
@ -256,10 +257,29 @@ pub trait Theme: Extension + ThemePage + Send + Sync {
/// Renderiza el contenido del `<head>` de la página.
///
/// Si se sobrescribe este método, se puede volver al comportamiento base con:
/// Si se sobrescribe este método, se puede volver al renderizado base con:
/// `<Self as ThemePage>::render_head(self, page)`.
#[inline]
fn render_page_head(&self, page: &mut Page) -> Markup {
if page.param_or("include_basic_assets", false) {
let pkg_version = env!("CARGO_PKG_VERSION");
page.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/normalize.css")
.with_version("8.0.1")
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/root.css")
.with_version(pkg_version)
.with_weight(-99),
))
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/css/basic.css")
.with_version(pkg_version)
.with_weight(-99),
));
}
<Self as ThemePage>::render_head(self, page)
}

View file

@ -1,4 +1,8 @@
# Basic theme, intro layout.
# Intro component.
intro_default_title = Hello, world!
intro_default_slogan = Discover⚡{ $app }
intro_default_button = A web solution powered by <strong>PageTop</strong>
intro_pagetop_label = PageTop version on Crates.io
intro_release_label = Release date
intro_license_label = License

View file

@ -1,16 +1,12 @@
welcome_extension_name = Default Homepage
welcome_extension_description = Displays a default homepage when none is configured.
welcome_page = Welcome page
welcome_title = Hello, world!
welcome_intro = Discover⚡{ $app }
welcome_powered = A web solution powered by <strong>PageTop</strong>
welcome_title = Welcome page
welcome_status_title = Status
welcome_status_1 = If you can see this page, it means the <strong>PageTop</strong> server is running correctly, but the application is not fully configured. This may be due to routine maintenance or a temporary issue.
welcome_status_2 = If the issue persists, please <strong>contact the system administrator</strong>.
welcome_support_title = Support
welcome_support_1 = To report issues with the <strong>PageTop</strong> framework, use <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">SoloGit</a>. Remember, before opening a new issue, review the existing ones to avoid duplicates.
welcome_support_1 = To report issues with the <strong>PageTop</strong> framework, use <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">GitHub</a>. Remember, before opening a new issue, review the existing ones to avoid duplicates.
welcome_support_2 = For issues specific to the application (<strong>{ $app }</strong>), please use its official repository or support channel.

View file

@ -1,4 +1,8 @@
# Basic theme, intro layout.
# Intro component.
intro_default_title = ¡Hola, mundo!
intro_default_slogan = Descubre⚡{ $app }
intro_default_button = Una solución web creada con <strong>PageTop</strong>
intro_pagetop_label = Versión de PageTop en Crates.io
intro_release_label = Lanzamiento
intro_license_label = Licencia

View file

@ -1,16 +1,12 @@
welcome_extension_name = Página de inicio predeterminada
welcome_extension_description = Muestra una página de inicio predeterminada cuando no hay ninguna configurada.
welcome_page = Página de bienvenida
welcome_title = ¡Hola, mundo!
welcome_intro = Descubre⚡{ $app }
welcome_powered = Una solución web creada con <strong>PageTop</strong>
welcome_title = Página de bienvenida
welcome_status_title = Estado
welcome_status_1 = Si puedes ver esta página, es porque el servidor de <strong>PageTop</strong> está funcionando correctamente, pero la aplicación no está completamente configurada. Esto puede deberse a tareas de mantenimiento o a una incidencia temporal.
welcome_status_2 = Si el problema persiste, por favor, <strong>contacta con el administrador del sistema</strong>.
welcome_support_title = Soporte
welcome_support_1 = Para comunicar incidencias del propio entorno <strong>PageTop</strong>, utiliza <a href="https://git.cillero.es/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">SoloGit</a>. Recuerda, antes de abrir una nueva incidencia, revisa las existentes para evitar duplicados.
welcome_support_1 = Para comunicar incidencias del propio entorno <strong>PageTop</strong>, utiliza <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank" rel="noreferrer">GitHub</a>. Recuerda, antes de abrir una nueva incidencia, revisa las existentes para evitar duplicados.
welcome_support_2 = Para fallos específicos de la aplicación (<strong>{ $app }</strong>), utiliza su repositorio oficial o su canal de soporte.

View file

@ -1,12 +1,24 @@
html {
min-height: 100%;
background-color: black;
: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);
}
body {
margin: auto;
.intro {
position: relative;
min-height: 100%;
min-width: 350px;
color: var(--intro-color);
background-color: var(--intro-bg-color);
@ -18,22 +30,22 @@ body {
align-items: center;
}
section {
.intro section {
position: relative;
text-align: center;
}
a {
.intro a {
color: currentColor;
text-decoration: underline;
transition: font-size 0.2s, text-decoration-color 0.2s;
}
a:focus-visible {
.intro a:focus-visible {
outline: var(--intro-focus-outline);
outline-offset: var(--intro-focus-outline-offset);
}
a:hover,
a:hover:visited {
.intro a:hover,
.intro a:hover:visited {
text-decoration-color: var(--intro-color-link);
}
@ -172,8 +184,8 @@ a:hover:visited {
justify-content: space-between;
font-size: 1.5rem;
line-height: 1.3;
text-decoration: none;
transition: transform 0.3s ease-in-out;
text-decoration: none !important;
transition: transform 0.3s ease-in-out !important;
position: relative;
overflow: hidden;
min-width: 28.875rem;
@ -307,7 +319,7 @@ a:hover:visited {
}
.intro-button__link:hover {
transition: all .5s;
transform: rotate(-3deg) scale(1.1);
transform: rotate(-3deg) scale(1.125);
}
}
@ -326,11 +338,11 @@ a:hover:visited {
background: #fff;
position: relative;
}
.region--content {
.intro-text__children {
padding: 2.5rem 1.063rem 0.75rem;
overflow: hidden;
}
.region--content p {
.intro-text__children p {
width: 100%;
line-height: 150%;
font-weight: 400;
@ -342,7 +354,7 @@ a:hover:visited {
font-size: 1.375rem;
line-height: 2rem;
}
.intro-button + .region--content {
.intro-button + .intro-text__children {
padding-top: 7rem;
}
}
@ -351,7 +363,7 @@ a:hover:visited {
padding-bottom: 9rem;;
}
.intro-text,
.region--content {
.intro-text__children {
border-radius: 0.75rem;
}
.intro-text {
@ -359,19 +371,19 @@ a:hover:visited {
max-width: 60rem;
margin: 0 auto 6rem;
}
.region--content {
.intro-text__children {
padding-left: 4.5rem;
padding-right: 4.5rem;
}
}
.region--content .block {
.intro-text__children .block {
position: relative;
}
.region--content .block__title {
.intro-text__children .block__title {
margin: 1em 0 .8em;
}
.region--content .block__title span {
.intro-text__children .block__title span {
display: inline-block;
padding: 10px 30px 14px;
margin: 30px 0 0 20px;
@ -382,7 +394,7 @@ a:hover:visited {
border-color: orangered;
transform: rotate(-3deg) translateY(-25%);
}
.region--content .block__title:before {
.intro-text__children .block__title:before {
content: "";
height: 5px;
position: absolute;
@ -395,7 +407,7 @@ a:hover:visited {
transform: rotate(2deg) translateY(-50%);
transform-origin: top left;
}
.region--content .block__title:after {
.intro-text__children .block__title:after {
content: "";
height: 70rem;
position: absolute;
@ -406,22 +418,28 @@ a:hover:visited {
background: var(--intro-bg-block-1);
transform: rotate(2deg);
}
.region--content .block:nth-of-type(5n+1) .block__title:after {
.intro-text__children .block:nth-of-type(5n+1) .block__title:after {
background: var(--intro-bg-block-1);
}
.region--content .block:nth-of-type(5n+2) .block__title:after {
.intro-text__children .block:nth-of-type(5n+2) .block__title:after {
background: var(--intro-bg-block-2);
}
.region--content .block:nth-of-type(5n+3) .block__title:after {
.intro-text__children .block:nth-of-type(5n+3) .block__title:after {
background: var(--intro-bg-block-3);
}
.region--content .block:nth-of-type(5n+4) .block__title:after {
.intro-text__children .block:nth-of-type(5n+4) .block__title:after {
background: var(--intro-bg-block-4);
}
.region--content .block:nth-of-type(5n+5) .block__title:after {
.intro-text__children .block:nth-of-type(5n+5) .block__title:after {
background: var(--intro-bg-block-5);
}
#intro-badges {
display: none;
margin-bottom: 1.1rem;
text-align: center;
}
/*
* Footer
*/
@ -474,9 +492,3 @@ a:hover:visited {
padding: 0 1rem 2rem;
}
}
/* PoweredBy component */
.poweredby a:visited {
color: var(--intro-color-gray);
}

View file

@ -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;