::render_head(self, page)`
pub trait ThemePage {
+ /// Renderiza el **contenedor** de una región concreta del `` de la página.
+ ///
+ /// Obtiene los componentes asociados a `region.key()` desde el contexto de la página y, si hay
+ /// salida, envuelve el contenido en un contenedor `` predefinido.
+ ///
+ /// Si la región **no produce contenido**, devuelve un `Markup` vacío.
+ #[inline]
+ fn render_region(&self, page: &mut Page, region: &Region) -> Markup {
+ html! {
+ @let output = page.context().render_components_of(region.key());
+ @if !output.is_empty() {
+ @let region_name = region.name();
+ div
+ id=(region_name)
+ class={ "region region--" (region_name) }
+ role="region"
+ aria-label=[region.label().lookup(page)]
+ {
+ (output)
+ }
+ }
+ }
+ }
+
/// Renderiza el **contenido interior** del `` de la página.
///
- /// Esta implementación 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.
+ /// Recorre `regions` en el **orden declarado** y, para cada región con contenido, delega en
+ /// [`render_region()`](Self::render_region) la generación del contenedor. Las regiones sin
+ /// contenido **no** producen salida. Se asume que cada identificador de región es **único**
+ /// dentro de la página.
///
/// La etiqueta `` no se incluye aquí; únicamente renderiza su contenido.
+ #[inline]
fn render_body(&self, page: &mut Page, regions: &[Region]) -> Markup {
html! {
@for region in regions {
- @let output = page.context().render_components_of(region.key());
- @if !output.is_empty() {
- @let region_name = region.name();
- div
- id=(region_name)
- class={ "region region--" (region_name) }
- role="region"
- aria-label=[region.label().lookup(page)]
- {
- (output)
- }
- }
+ (self.render_region(page, region))
}
}
}
/// Renderiza el **contenido interior** del `` de la página.
///
- /// Recorre y genera por defecto las etiquetas básicas (`charset`, `title`, `description`,
- /// `viewport`, `X-UA-Compatible`), los metadatos (`name/content`) y propiedades
- /// (`property/content`), además de los recursos CSS/JS de la página.
+ /// Incluye por defecto las etiquetas básicas (`charset`, `title`, `description`, `viewport`,
+ /// `X-UA-Compatible`), los metadatos (`name/content`) y propiedades (`property/content`),
+ /// además de los recursos CSS/JS de la página.
///
- /// La etiqueta `` no se incluye aquí; únicamente renderiza su contenido.
+ /// La etiqueta `` no se incluye aquí; únicamente se renderiza su contenido.
+ #[inline]
fn render_head(&self, page: &mut Page) -> Markup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! {
@@ -152,6 +172,19 @@ pub trait Theme: Extension + ThemePage + Send + Sync {
&*REGIONS
}
+ /// Renderiza una región de la página **por clave**.
+ ///
+ /// Busca en [`page_regions()`](Self::page_regions) la región asociada a una clave y, si existe,
+ /// delega en [`ThemePage::render_region()`] su renderizado. Si no se encuentra la clave o la
+ /// región no produce contenido, devuelve un `Markup` vacío.
+ fn render_page_region(&self, page: &mut Page, key: &str) -> Markup {
+ html! {
+ @if let Some(region) = self.page_regions().iter().find(|r| r.key() == key) {
+ (self.render_region(page, region))
+ }
+ }
+ }
+
/// Acciones específicas del tema antes de renderizar el `` de la página.
///
/// Útil para preparar clases, inyectar recursos o ajustar metadatos.
diff --git a/static/css/basic.css b/static/css/basic.css
index 04801dd..058e173 100644
--- a/static/css/basic.css
+++ b/static/css/basic.css
@@ -1,3 +1,19 @@
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ font-family: var(--val-font-family);
+ font-size: var(--val-fs--base);
+ font-weight: var(--val-fw--base);
+ line-height: var(--val-lh--base);
+ color: var(--val-color--text);
+ background-color: var(--val-color--bg);
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: transparent;
+}
+
/* Page layout */
.region--footer {
diff --git a/static/css/intro.css b/static/css/intro.css
index 39c9d6a..774bbb2 100644
--- a/static/css/intro.css
+++ b/static/css/intro.css
@@ -1,37 +1,15 @@
-:root {
- --bg-img: url('/img/intro-header.jpg');
- --bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--bg-img) type('image/jpeg'));
- --bg-img-sm: url('/img/intro-header-sm.jpg');
- --bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--bg-img-sm) type('image/jpeg'));
- --bg-color: #8c5919;
- --color: #1a202c;
- --color-gray: #e4e4e7;
- --color-link: #1e4eae;
- --color-block-1: #b689ff;
- --color-block-2: #fecaca;
- --color-block-3: #e6a9e2;
- --color-block-4: #ffedca;
- --color-block-5: #ffffff;
- --focus-outline: 2px solid var(--color-link);
- --focus-outline-offset: 2px;
- --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
-}
-
html {
min-height: 100%;
background-color: black;
}
+
body {
margin: auto;
position: relative;
min-height: 100%;
min-width: 350px;
- background-color: var(--bg-color);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- font-size: 1.125rem;
- font-weight: 300;
- color: var(--color);
- line-height: 1.6;
+ color: var(--intro-color);
+ background-color: var(--intro-bg-color);
width: 100%;
display: flex;
@@ -51,12 +29,12 @@ a {
transition: font-size 0.2s, text-decoration-color 0.2s;
}
a:focus-visible {
- outline: var(--focus-outline);
- outline-offset: var(--focus-outline-offset);
+ outline: var(--intro-focus-outline);
+ outline-offset: var(--intro-focus-outline-offset);
}
a:hover,
a:hover:visited {
- text-decoration-color: var(--color-link);
+ text-decoration-color: var(--intro-color-link);
}
/*
@@ -69,9 +47,9 @@ a:hover:visited {
width: 100%;
max-width: 80rem;
margin: 0 auto;
- padding-bottom: 9rem;
- background-image: var(--bg-img-sm);
- background-image: var(--bg-img-sm-set);
+ padding-bottom: 4rem;
+ background-image: var(--intro-bg-img-sm);
+ background-image: var(--intro-bg-img-sm-set);
background-position: top center;
background-position-y: -1rem;
background-size: contain;
@@ -119,8 +97,8 @@ a:hover:visited {
}
@media (min-width: 64rem) {
.intro-header {
- background-image: var(--bg-img);
- background-image: var(--bg-img-set);
+ background-image: var(--intro-bg-img);
+ background-image: var(--intro-bg-img-set);
}
.intro-header__title {
padding: 1.2rem 2rem 2.6rem 2rem;
@@ -180,8 +158,7 @@ a:hover:visited {
.intro-button {
width: 100%;
- margin: 0 auto 3rem;
- z-index: 10;
+ margin: 0 auto;
}
.intro-button__link {
background: #7f1d1d;
@@ -306,6 +283,9 @@ a:hover:visited {
animation-play-state: paused;
}
@media (max-width: 48rem) {
+ .intro-header {
+ padding-bottom: 9rem;;
+ }
.intro-button__link {
height: 6.25rem;
min-width: auto;
@@ -342,17 +322,15 @@ a:hover:visited {
font-size: 1.3125rem;
font-weight: 400;
line-height: 1.5;
- margin-top: -6rem;
margin-bottom: 0;
background: #fff;
position: relative;
+}
+.region--content {
padding: 2.5rem 1.063rem 0.75rem;
overflow: hidden;
}
-.intro-button + .intro-text {
- padding-top: 6rem;
-}
-.intro-text p {
+.region--content p {
width: 100%;
line-height: 150%;
font-weight: 400;
@@ -364,31 +342,39 @@ a:hover:visited {
font-size: 1.375rem;
line-height: 2rem;
}
- .intro-button + .intro-text {
+ .intro-button + .region--content {
padding-top: 7rem;
}
}
@media (min-width: 64rem) {
- .intro-text {
+ .intro-header {
+ padding-bottom: 9rem;;
+ }
+ .intro-text,
+ .region--content {
border-radius: 0.75rem;
- box-shadow: var(--shadow);
+ }
+ .intro-text {
+ box-shadow: var(--intro-shadow);
max-width: 60rem;
margin: 0 auto 6rem;
+ }
+ .region--content {
padding-left: 4.5rem;
padding-right: 4.5rem;
}
}
-.intro-text .block {
+.region--content .block {
position: relative;
}
-.intro-text .block__title {
+.region--content .block__title {
margin: 1em 0 .8em;
}
-.intro-text .block__title span {
+.region--content .block__title span {
display: inline-block;
padding: 10px 30px 14px;
- margin: 0 0 0 20px;
+ margin: 30px 0 0 20px;
background: white;
border: 5px solid;
border-radius: 30px;
@@ -396,7 +382,7 @@ a:hover:visited {
border-color: orangered;
transform: rotate(-3deg) translateY(-25%);
}
-.intro-text .block__title:before {
+.region--content .block__title:before {
content: "";
height: 5px;
position: absolute;
@@ -409,7 +395,7 @@ a:hover:visited {
transform: rotate(2deg) translateY(-50%);
transform-origin: top left;
}
-.intro-text .block__title:after {
+.region--content .block__title:after {
content: "";
height: 70rem;
position: absolute;
@@ -417,23 +403,23 @@ a:hover:visited {
left: -15%;
width: 130%;
z-index: -10;
- background: var(--color-block-1);
+ background: var(--intro-bg-block-1);
transform: rotate(2deg);
}
-.intro-text .block:nth-of-type(5n+1) .block__title:after {
- background: var(--color-block-1);
+.region--content .block:nth-of-type(5n+1) .block__title:after {
+ background: var(--intro-bg-block-1);
}
-.intro-text .block:nth-of-type(5n+2) .block__title:after {
- background: var(--color-block-2);
+.region--content .block:nth-of-type(5n+2) .block__title:after {
+ background: var(--intro-bg-block-2);
}
-.intro-text .block:nth-of-type(5n+3) .block__title:after {
- background: var(--color-block-3);
+.region--content .block:nth-of-type(5n+3) .block__title:after {
+ background: var(--intro-bg-block-3);
}
-.intro-text .block:nth-of-type(5n+4) .block__title:after {
- background: var(--color-block-4);
+.region--content .block:nth-of-type(5n+4) .block__title:after {
+ background: var(--intro-bg-block-4);
}
-.intro-text .block:nth-of-type(5n+5) .block__title:after {
- background: var(--color-block-5);
+.region--content .block:nth-of-type(5n+5) .block__title:after {
+ background: var(--intro-bg-block-5);
}
/*
@@ -443,7 +429,7 @@ a:hover:visited {
.intro-footer {
width: 100%;
background-color: black;
- color: var(--color-gray);
+ color: var(--intro-color-gray);
padding-bottom: 2rem;
}
@@ -459,7 +445,7 @@ a:hover:visited {
line-height: 100%;
}
.intro-footer__body a:visited {
- color: var(--color-gray);
+ color: var(--intro-color-gray);
}
.intro-footer__logo,
.intro-footer__links {
@@ -492,5 +478,5 @@ a:hover:visited {
/* PoweredBy component */
.poweredby a:visited {
- color: var(--color-gray);
+ color: var(--intro-color-gray);
}
diff --git a/static/css/menu.css b/static/css/menu.css
index 5520e39..6522f4a 100644
--- a/static/css/menu.css
+++ b/static/css/menu.css
@@ -1,54 +1,92 @@
+/* Aislamiento & normalización */
+
.menu {
+ isolation: isolate;
+}
+@supports (all: revert) {
+ .menu {
+ all: revert;
+ display: block; }
+}
+.menu {
+ box-sizing: border-box;
+ line-height: var(--val-menu--line-height, 1.5);
+ color: var(--val-color--text);
+ text-align: left;
+ text-transform: none;
+ letter-spacing: normal;
+ word-spacing: normal;
+ white-space: normal;
+ cursor: default;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
width: 100%;
height: auto;
margin: 0;
padding: 0;
z-index: 9999;
- border: none;
- outline: none;
+ border: 0;
background: var(--val-menu--color-bg);
}
+.menu *,
+.menu *::before,
+.menu *::after {
+ box-sizing: inherit;
+}
+.menu :where(a, button) {
+ appearance: none;
+ background: none;
+ border: 0;
+ font: inherit;
+ color: inherit;
+ text-decoration: none;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.menu :where(a, button):focus-visible {
+ outline: 2px solid var(--val-menu--color-highlight);
+ outline-offset: 2px;
+}
+.menu :where(ul, ol) {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+.menu svg {
+ fill: currentColor;
+}
+
+/* Estructura */
.menu__wrapper {
padding-right: var(--val-gap);
}
-.menu__wrapper a,
-.menu__wrapper button {
- cursor: pointer;
- border: none;
- background: none;
- text-decoration: none;
-}
-.menu__nav ul {
- margin: 0;
- padding: 0;
-}
.menu__nav li {
display: inline-block;
- margin: 0 0 0 1.5rem;
+ margin: 0;
+ margin-inline-start: 1.5rem;
padding: 0;
line-height: var(--val-menu--item-height);
list-style: none;
- list-style-type: none;
}
.menu__item--label,
-.menu__nav li > a {
+.menu__nav li > .menu__link {
position: relative;
- font-weight: 500;
- color: var(--val-color--text);
+ font-weight: normal;
text-rendering: optimizeLegibility;
+ font-size: 1.45rem;
}
-.menu__nav li > a {
- border: none;
+.menu__nav li > .menu__link {
transition: color 0.3s ease-in-out;
}
-.menu__nav li:hover > a,
-.menu__nav li > a:focus {
+.menu__nav li:hover > .menu__link,
+.menu__nav li > .menu__link:focus {
color: var(--val-menu--color-highlight);
}
-.menu__nav li > a > svg.icon {
+.menu__nav li > .menu__link > svg.icon {
margin-left: 0.25rem;
}
@@ -57,19 +95,18 @@
max-width: 100%;
height: auto;
padding: var(--val-gap-0-5) var(--val-gap-1-5);
- border: none;
- outline: none;
+ border: 0;
background: var(--val-menu--color-bg);
border-top: 3px solid var(--val-menu--color-highlight);
z-index: 500;
opacity: 0;
visibility: hidden;
box-shadow: 0 4px 6px -1px var(--val-menu--color-border), 0 2px 4px -1px var(--val-menu--color-shadow);
- transition: all 0.5s ease-in-out;
+ transition: all 0.3s ease-in-out;
}
.menu__item--children:hover > .menu__children,
-.menu__item--children > a:focus + .menu__children,
+.menu__item--children > .menu__link:focus + .menu__children,
.menu__item--children .menu__children:focus-within {
margin-top: 0.4rem;
opacity: 1;
@@ -81,14 +118,12 @@
max-width: var(--val-menu--item-width-max);
}
.menu__submenu-title {
- font-family: inherit;
font-size: 1rem;
- font-weight: 500;
+ font-weight: normal;
margin: 0;
padding: var(--val-menu--line-padding) 0;
line-height: var(--val-menu--line-height);
- border: none;
- outline: none;
+ border: 0;
color: var(--val-menu--color-highlight);
text-transform: uppercase;
text-rendering: optimizeLegibility;
@@ -113,18 +148,15 @@
display: none;
}
-/* Applies <= 992px */
-@media only screen and (max-width: 62rem) {
+/* Responsive <= 62rem (992px) */
+
+@media (max-width: 62rem) {
.menu__wrapper {
padding-right: var(--val-gap-0-5);
}
.menu__trigger {
- cursor: pointer;
width: var(--val-menu--trigger-width);
height: var(--val-menu--item-height);
- border: none;
- outline: none;
- background: none;
display: flex;
flex-direction: column;
justify-content: center;
@@ -133,6 +165,13 @@
width: 2rem;
height: 2rem;
}
+
+ .menu__nav,
+ .menu__children {
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+ }
+
.menu__nav {
position: fixed;
top: 0;
@@ -143,10 +182,16 @@
overflow: hidden;
background: var(--val-menu--color-bg);
transform: translate(-100%);
- transition: all 0.5s ease-in-out;
+ transition: transform .5s ease-in-out, opacity .5s ease-in-out;
+ will-change: transform;
+ backface-visibility: hidden;
+ visibility: hidden;
+ pointer-events: none;
}
.menu__nav.active {
transform: translate(0%);
+ visibility: visible;
+ pointer-events: auto;
}
.menu__nav li {
@@ -156,16 +201,18 @@
}
.menu__item--label,
- .menu__nav li > a {
+ .menu__nav li > .menu__link {
display: block;
+ text-align: inherit;
+ width: 100%;
padding: var(--val-menu--line-padding) var(--val-menu--item-height) var(--val-menu--line-padding) var(--val-menu--item-gap);
border-bottom: 1px solid var(--val-menu--color-border);
}
.menu__nav li ul li.menu__item--label,
- .menu__nav li ul li > a {
+ .menu__nav li ul li > .menu__link {
border-bottom: 0;
}
- .menu__nav li > a > svg.icon {
+ .menu__nav li > .menu__link > svg.icon {
position: absolute;
top: var(--val-menu--line-padding);
right: var(--val-menu--line-padding);
@@ -191,6 +238,7 @@
visibility: visible;
transform: translateX(0%);
box-shadow: none;
+ transition: opacity .5s ease-in-out, transform .5s ease-in-out, margin-top .5s ease-in-out;
}
.menu__children.active {
display: block;
@@ -223,6 +271,16 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ font-size: 1.45rem;
+ font-weight: normal;
+ opacity: 0;
+ transform: translateY(.25rem);
+ transition: opacity .5s ease-in-out, transform .5s ease-in-out;
+ will-change: opacity, transform;
+ }
+ .menu__header.active .menu__title {
+ opacity: 1;
+ transform: translateY(0);
}
.menu__close,
.menu__back {
@@ -231,18 +289,20 @@
height: var(--val-menu--item-height);
line-height: var(--val-menu--item-height);
color: var(--val-color--text);
- cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
+ background: var(--val-menu--color-bg);
}
.menu__close {
font-size: 2.25rem;
- border-left: 1px solid var(--val-menu--color-border) !important;
+ border: 1px solid var(--val-menu--color-border) !important;
+ border-width: 0 0 1px 1px !important;
}
.menu__back {
font-size: 1.25rem;
- border-right: 1px solid var(--val-menu--color-border) !important;
+ border: 1px solid var(--val-menu--color-border) !important;
+ border-width: 0 1px 1px 0 !important;
display: none;
}
.menu__header.active .menu__back {
@@ -267,15 +327,34 @@
opacity: 0;
visibility: hidden;
background: rgba(0, 0, 0, 0.55);
- transition: all 0.5s ease-in-out;
+ transition: opacity .5s ease-in-out, visibility 0s linear .5s;
}
.menu__overlay.active {
opacity: 1;
visibility: visible;
+ transition-delay: 0s, 0s;
+ }
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .menu__item--children:hover > .menu__children {
+ margin-top: 0.4rem;
+ opacity: 1;
+ visibility: visible;
}
}
-/* ANIMATIONS */
+@media (prefers-reduced-motion: reduce) {
+ .menu__nav,
+ .menu__children,
+ .menu__title,
+ .menu__overlay {
+ transition: none !important;
+ animation: none !important;
+ }
+}
+
+/* Animaciones */
@keyframes slideLeft {
0% {
diff --git a/static/css/root.css b/static/css/root.css
index aeab1c6..270c1b3 100644
--- a/static/css/root.css
+++ b/static/css/root.css
@@ -1,3 +1,22 @@
+:root {
+ --intro-bg-img: url('/img/intro-header.jpg');
+ --intro-bg-img-set: image-set(url('/img/intro-header.avif') type('image/avif'), url('/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg'));
+ --intro-bg-img-sm: url('/img/intro-header-sm.jpg');
+ --intro-bg-img-sm-set: image-set(url('/img/intro-header-sm.avif') type('image/avif'), url('/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg'));
+ --intro-bg-color: #8c5919;
+ --intro-bg-block-1: #b689ff;
+ --intro-bg-block-2: #fecaca;
+ --intro-bg-block-3: #e6a9e2;
+ --intro-bg-block-4: #ffedca;
+ --intro-bg-block-5: #ffffff;
+ --intro-color: #1a202c;
+ --intro-color-gray: #e4e4e7;
+ --intro-color-link: #1e4eae;
+ --intro-focus-outline: 2px solid var(--intro-color-link);
+ --intro-focus-outline-offset: 2px;
+ --intro-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+}
+
:root {
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--val-font-serif: "Lora","georgia",serif;
diff --git a/static/js/menu.js b/static/js/menu.js
index 6b5ae1b..1f09bfe 100644
--- a/static/js/menu.js
+++ b/static/js/menu.js
@@ -48,7 +48,6 @@ function menu__reset(menu, nav, overlay) {
}
document.querySelectorAll('.menu').forEach(menu => {
-
let menuChildren = [];
const menuNav = menu.querySelector('.menu__nav');
const menuOverlay = menu.querySelector('.menu__overlay');