♻️ Refactoriza la gestión de regiones y plantillas
This commit is contained in:
parent
0746bbbee7
commit
9f79e4179a
15 changed files with 494 additions and 655 deletions
|
|
@ -121,7 +121,7 @@ impl Theme for Aliner {
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
Region::FOOTER,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,8 @@
|
||||||
//! Componentes nativos proporcionados por PageTop.
|
//! Componentes nativos proporcionados por PageTop.
|
||||||
//!
|
|
||||||
//! Conviene destacar que PageTop distingue entre:
|
|
||||||
//!
|
|
||||||
//! - **Componentes estructurales** que definen el esqueleto de un documento HTML, como [`Template`]
|
|
||||||
//! y [`Region`], utilizados por [`Page`](crate::response::page::Page) para generar la estructura
|
|
||||||
//! final.
|
|
||||||
//! - **Componentes de contenido** (menús, barras, tarjetas, etc.), que se incluyen en las regiones
|
|
||||||
//! gestionadas por los componentes estructurales.
|
|
||||||
//!
|
|
||||||
//! El componente [`Template`] describe cómo maquetar el cuerpo del documento a partir de varias
|
|
||||||
//! regiones lógicas ([`Region`]). En función de la plantilla seleccionada, determina qué regiones
|
|
||||||
//! se renderizan y en qué orden. Por ejemplo, la plantilla predeterminada [`Template::DEFAULT`]
|
|
||||||
//! utiliza las regiones [`Region::HEADER`], [`Region::CONTENT`] y [`Region::FOOTER`].
|
|
||||||
//!
|
|
||||||
//! Un componente [`Region`] es un contenedor lógico asociado a un nombre de región. Su contenido se
|
|
||||||
//! obtiene del [`Context`](crate::core::component::Context), donde los componentes se registran
|
|
||||||
//! mediante [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in) y
|
|
||||||
//! otros mecanismos similares, y se integra en el documento a través de [`Template`].
|
|
||||||
//!
|
|
||||||
//! Por su parte, una página ([`Page`](crate::response::page::Page)) representa un documento HTML
|
|
||||||
//! completo. Implementa [`Contextual`](crate::core::component::Contextual) para mantener su propio
|
|
||||||
//! [`Context`](crate::core::component::Context), donde gestiona el tema activo, la plantilla
|
|
||||||
//! seleccionada y los componentes asociados a cada región, y se encarga de generar la estructura
|
|
||||||
//! final de la página.
|
|
||||||
//!
|
|
||||||
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
|
|
||||||
//! registran componentes en el [`Context`](crate::core::component::Context), las plantillas
|
|
||||||
//! organizan las regiones y las páginas generan el documento HTML resultante.
|
|
||||||
//!
|
|
||||||
//! Los temas pueden sobrescribir [`Template`] para exponer nuevas plantillas o adaptar las
|
|
||||||
//! predeterminadas, y lo mismo con [`Region`] para añadir regiones adicionales o personalizar su
|
|
||||||
//! representación.
|
|
||||||
|
|
||||||
mod html;
|
mod html;
|
||||||
pub use html::Html;
|
pub use html::Html;
|
||||||
|
|
||||||
mod region;
|
|
||||||
pub use region::Region;
|
|
||||||
|
|
||||||
mod template;
|
|
||||||
pub use template::Template;
|
|
||||||
|
|
||||||
mod block;
|
mod block;
|
||||||
pub use block::Block;
|
pub use block::Block;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente estructural que renderiza el contenido de una región del documento.
|
|
||||||
///
|
|
||||||
/// `Region` actúa como un contenedor lógico asociado a un nombre de región. Su contenido se obtiene
|
|
||||||
/// del contexto de renderizado ([`Context`]), donde los componentes suelen registrarse con métodos
|
|
||||||
/// como [`Contextual::with_child_in()`]. Cada región puede integrarse posteriormente en el cuerpo
|
|
||||||
/// del documento mediante [`Template`], normalmente desde una página ([`Page`]).
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Region {
|
|
||||||
#[default(AttrName::new(Self::DEFAULT))]
|
|
||||||
name: AttrName,
|
|
||||||
#[default(L10n::l("region-content"))]
|
|
||||||
label: L10n,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Region {
|
|
||||||
fn new() -> Self {
|
|
||||||
Region::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.name.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let Some(name) = self.name().get() else {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
};
|
|
||||||
let output = cx.render_region(&name);
|
|
||||||
if output.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
div
|
|
||||||
id=[self.id()]
|
|
||||||
class=(join!("region region-", &name))
|
|
||||||
role="region"
|
|
||||||
aria-label=[self.label().lookup(cx)]
|
|
||||||
{
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Region {
|
|
||||||
/// Región especial situada al **inicio del documento**.
|
|
||||||
///
|
|
||||||
/// Su función es proporcionar un punto estable donde las extensiones puedan inyectar contenido
|
|
||||||
/// global antes de renderizar el resto de regiones principales (cabecera, contenido, etc.).
|
|
||||||
///
|
|
||||||
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
|
||||||
/// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o
|
|
||||||
/// contenido de depuración que deban situarse en la parte superior del documento.
|
|
||||||
///
|
|
||||||
/// Se considera una región **reservada** para este tipo de usos globales.
|
|
||||||
pub const PAGETOP: &str = "page-top";
|
|
||||||
|
|
||||||
/// Región estándar para la **cabecera** del documento.
|
|
||||||
///
|
|
||||||
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
|
||||||
pub const HEADER: &str = "header";
|
|
||||||
|
|
||||||
/// Región principal de **contenido**.
|
|
||||||
///
|
|
||||||
/// Es la región donde se espera que se renderice el contenido principal de la página (p. ej.
|
|
||||||
/// cuerpo de la ruta actual, bloques centrales, vistas principales, etc.). En muchos temas será
|
|
||||||
/// la región mínima imprescindible para que la página tenga sentido.
|
|
||||||
pub const CONTENT: &str = "content";
|
|
||||||
|
|
||||||
/// Región estándar para el **pie de página**.
|
|
||||||
///
|
|
||||||
/// Suele contener información legal, enlaces secundarios, créditos, etc.
|
|
||||||
pub const FOOTER: &str = "footer";
|
|
||||||
|
|
||||||
/// Región especial situada al **final del documento**.
|
|
||||||
///
|
|
||||||
/// Pensada para proporcionar un punto estable donde las extensiones puedan inyectar contenido
|
|
||||||
/// global después de renderizar el resto de regiones principales (cabecera, contenido, etc.).
|
|
||||||
///
|
|
||||||
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
|
||||||
/// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos
|
|
||||||
/// que deban situarse en la parte inferior del documento.
|
|
||||||
///
|
|
||||||
/// Igual que [`Self::PAGETOP`], se considera una región **reservada** para este tipo de usos
|
|
||||||
/// globales.
|
|
||||||
pub const PAGEBOTTOM: &str = "page-bottom";
|
|
||||||
|
|
||||||
/// Región por defecto que se asigna cuando no se especifica ningún nombre.
|
|
||||||
///
|
|
||||||
/// Por diseño, la región por defecto es la de contenido principal ([`Self::CONTENT`]), de
|
|
||||||
/// manera que un tema sencillo pueda limitarse a definir una sola región funcional.
|
|
||||||
pub const DEFAULT: &str = Self::CONTENT;
|
|
||||||
|
|
||||||
/// Prepara una región para el nombre indicado.
|
|
||||||
///
|
|
||||||
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
|
|
||||||
/// contenedor. Al renderizarse, este componente mostrará el contenido registrado en el contexto
|
|
||||||
/// bajo ese nombre.
|
|
||||||
pub fn named(name: impl AsRef<str>) -> Self {
|
|
||||||
Region {
|
|
||||||
name: AttrName::new(name),
|
|
||||||
label: L10n::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepara una región para el nombre indicado con una etiqueta de accesibilidad.
|
|
||||||
///
|
|
||||||
/// El valor de `name` se utiliza como nombre de la región y como identificador (`id`) del
|
|
||||||
/// contenedor, mientras que `label` será el texto localizado que se usará como `aria-label` del
|
|
||||||
/// contenedor.
|
|
||||||
pub fn labeled(name: impl AsRef<str>, label: L10n) -> Self {
|
|
||||||
Region {
|
|
||||||
name: AttrName::new(name),
|
|
||||||
label,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Region BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece o modifica el nombre de la región.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
|
||||||
self.name.alter_value(name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la etiqueta localizada de la región.
|
|
||||||
///
|
|
||||||
/// Esta etiqueta se utiliza como `aria-label` del contenedor predefinido `<div role="region">`,
|
|
||||||
/// lo que mejora la accesibilidad para lectores de pantalla y otras tecnologías de apoyo.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_label(mut self, label: L10n) -> Self {
|
|
||||||
self.label = label;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Region GETTERS >*************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve el nombre de la región.
|
|
||||||
pub fn name(&self) -> &AttrName {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la etiqueta localizada asociada a la región.
|
|
||||||
pub fn label(&self) -> &L10n {
|
|
||||||
&self.label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente estructural para renderizar plantillas de contenido.
|
|
||||||
///
|
|
||||||
/// `Template` describe cómo se compone el cuerpo del documento a partir de varias regiones lógicas
|
|
||||||
/// ([`Region`]). En función de su nombre, decide qué regiones se renderizan y en qué orden.
|
|
||||||
///
|
|
||||||
/// Normalmente se invoca desde una página ([`Page`]), que consulta el nombre de plantilla guardado
|
|
||||||
/// en el [`Context`] y delega en `Template` la composición de las regiones que forman el cuerpo del
|
|
||||||
/// documento.
|
|
||||||
///
|
|
||||||
/// Los temas pueden sobrescribir este componente para exponer sus propias plantillas o adaptar las
|
|
||||||
/// plantillas predeterminadas.
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Template {
|
|
||||||
#[default(AttrName::new(Self::DEFAULT))]
|
|
||||||
name: AttrName,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Template {
|
|
||||||
fn new() -> Self {
|
|
||||||
Template::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.name.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let Some(name) = self.name().get() else {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
};
|
|
||||||
match name.as_str() {
|
|
||||||
Self::DEFAULT | Self::ERROR => PrepareMarkup::With(html! {
|
|
||||||
(Region::labeled(Region::HEADER, L10n::l("region-header")).render(cx))
|
|
||||||
(Region::default().render(cx))
|
|
||||||
(Region::labeled(Region::FOOTER, L10n::l("region-footer")).render(cx))
|
|
||||||
}),
|
|
||||||
_ => PrepareMarkup::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Template {
|
|
||||||
/// Nombre de la plantilla predeterminada.
|
|
||||||
///
|
|
||||||
/// Por defecto define una estructura básica con las regiones [`Region::HEADER`],
|
|
||||||
/// [`Region::CONTENT`] y [`Region::FOOTER`], en ese orden. Esta plantilla se usa cuando no se
|
|
||||||
/// selecciona ninguna otra de forma explícita (ver [`Contextual::with_template()`]).
|
|
||||||
pub const DEFAULT: &str = "default";
|
|
||||||
|
|
||||||
/// Nombre de la plantilla de error.
|
|
||||||
///
|
|
||||||
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto reutiliza
|
|
||||||
/// la misma estructura que [`Self::DEFAULT`], pero permite a temas y extensiones distinguir
|
|
||||||
/// el contexto de error para aplicar estilos o contenidos específicos.
|
|
||||||
pub const ERROR: &str = "error";
|
|
||||||
|
|
||||||
/// Selecciona la plantilla asociada al nombre indicado.
|
|
||||||
///
|
|
||||||
/// El valor de `name` se utiliza como nombre de la plantilla y como identificador (`id`) del
|
|
||||||
/// componente.
|
|
||||||
pub fn named(name: impl AsRef<str>) -> Self {
|
|
||||||
Template {
|
|
||||||
name: AttrName::new(name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Template BUILDER >***********************************************************************
|
|
||||||
|
|
||||||
/// Establece o modifica el nombre de la plantilla seleccionada.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
|
||||||
self.name.alter_value(name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Template GETTERS >***********************************************************************
|
|
||||||
|
|
||||||
/// Devuelve el nombre de la plantilla seleccionada.
|
|
||||||
pub fn name(&self) -> &AttrName {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,7 @@ impl Theme for Basic {
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
Region::FOOTER,
|
&DefaultRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::base::component::Template;
|
|
||||||
use crate::core::component::ChildOp;
|
use crate::core::component::ChildOp;
|
||||||
use crate::core::theme::all::DEFAULT_THEME;
|
use crate::core::theme::all::DEFAULT_THEME;
|
||||||
use crate::core::theme::{ChildrenInRegions, ThemeRef};
|
use crate::core::theme::{ChildrenInRegions, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::{html, Markup};
|
use crate::html::{html, Markup};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
|
|
@ -68,7 +67,7 @@ pub enum ContextError {
|
||||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||||
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
||||||
/// .with_theme(&Aliner)
|
/// .with_theme(&Aliner)
|
||||||
/// .with_template(Template::DEFAULT)
|
/// .with_template(&DefaultTemplate::Standard)
|
||||||
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
/// .with_assets(ContextOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||||
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
/// .with_assets(ContextOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||||
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||||
|
|
@ -92,7 +91,7 @@ pub trait Contextual: LangId {
|
||||||
|
|
||||||
/// Especifica la plantilla para renderizar el documento.
|
/// Especifica la plantilla para renderizar el documento.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(self, template_name: &'static str) -> Self;
|
fn with_template(self, template: TemplateRef) -> Self;
|
||||||
|
|
||||||
/// Añade o modifica un parámetro dinámico del contexto.
|
/// Añade o modifica un parámetro dinámico del contexto.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
|
|
@ -102,9 +101,9 @@ pub trait Contextual: LangId {
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_assets(self, op: ContextOp) -> Self;
|
fn with_assets(self, op: ContextOp) -> Self;
|
||||||
|
|
||||||
/// Opera con [`ChildOp`] en una región (`region_name`) del documento.
|
/// Opera con [`ChildOp`] en una región del documento.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(self, region_name: impl AsRef<str>, op: ChildOp) -> Self;
|
fn with_child_in(self, region_ref: RegionRef, op: ChildOp) -> Self;
|
||||||
|
|
||||||
// **< Contextual GETTERS >*********************************************************************
|
// **< Contextual GETTERS >*********************************************************************
|
||||||
|
|
||||||
|
|
@ -114,8 +113,8 @@ pub trait Contextual: LangId {
|
||||||
/// Devuelve el tema que se usará para renderizar el documento.
|
/// Devuelve el tema que se usará para renderizar el documento.
|
||||||
fn theme(&self) -> ThemeRef;
|
fn theme(&self) -> ThemeRef;
|
||||||
|
|
||||||
/// Devuelve el nombre de la plantilla usada para renderizar el documento.
|
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||||
fn template(&self) -> &str;
|
fn template(&self) -> TemplateRef;
|
||||||
|
|
||||||
/// Recupera un parámetro como [`Option`].
|
/// Recupera un parámetro como [`Option`].
|
||||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||||
|
|
@ -208,7 +207,7 @@ pub struct Context {
|
||||||
request : Option<HttpRequest>, // Solicitud HTTP de origen.
|
request : Option<HttpRequest>, // Solicitud HTTP de origen.
|
||||||
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
||||||
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
theme : ThemeRef, // Referencia al tema usado para renderizar.
|
||||||
template : &'static str, // Nombre de la plantilla usada para renderizar.
|
template : TemplateRef, // Plantilla usada para renderizar.
|
||||||
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
favicon : Option<Favicon>, // Favicon, si se ha definido.
|
||||||
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||||
|
|
@ -248,7 +247,7 @@ impl Context {
|
||||||
request,
|
request,
|
||||||
langid,
|
langid,
|
||||||
theme : *DEFAULT_THEME,
|
theme : *DEFAULT_THEME,
|
||||||
template : Template::DEFAULT,
|
template : DEFAULT_THEME.default_template(),
|
||||||
favicon : None,
|
favicon : None,
|
||||||
stylesheets: Assets::<StyleSheet>::new(),
|
stylesheets: Assets::<StyleSheet>::new(),
|
||||||
javascripts: Assets::<JavaScript>::new(),
|
javascripts: Assets::<JavaScript>::new(),
|
||||||
|
|
@ -286,10 +285,10 @@ impl Context {
|
||||||
markup
|
markup
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renderiza los componentes de la región `region_name`.
|
/// Renderiza los componentes de una región.
|
||||||
pub fn render_region(&mut self, region_name: impl AsRef<str>) -> Markup {
|
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
|
||||||
self.regions
|
self.regions
|
||||||
.children_for(self.theme, region_name)
|
.children_for(self.theme, region_ref)
|
||||||
.render(self)
|
.render(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,8 +416,8 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(mut self, template_name: &'static str) -> Self {
|
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||||
self.template = template_name;
|
self.template = template;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -474,8 +473,8 @@ impl Contextual for Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||||
self.regions.alter_child_in(region_name, op);
|
self.regions.alter_child_in(region_ref, op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,7 +488,7 @@ impl Contextual for Context {
|
||||||
self.theme
|
self.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
fn template(&self) -> &str {
|
fn template(&self) -> TemplateRef {
|
||||||
self.template
|
self.template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,23 @@ use crate::core::AnyInfo;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::{actions_boxed, service};
|
use crate::{actions_boxed, service};
|
||||||
|
|
||||||
/// Representa una referencia a una extensión.
|
|
||||||
///
|
|
||||||
/// Las extensiones se definen como instancias estáticas globales para poder acceder a ellas desde
|
|
||||||
/// cualquier hilo de la ejecución sin necesidad de sincronización adicional.
|
|
||||||
pub type ExtensionRef = &'static dyn Extension;
|
|
||||||
|
|
||||||
/// Interfaz común que debe implementar cualquier extensión de PageTop.
|
/// Interfaz común que debe implementar cualquier extensión de PageTop.
|
||||||
///
|
///
|
||||||
/// Este *trait* es fácil de implementar, basta con declarar una estructura de tamaño cero para la
|
/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la
|
||||||
/// extensión y sobreescribir los métodos que sea necesario.
|
/// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct Blog;
|
/// pub struct Blog;
|
||||||
///
|
///
|
||||||
/// impl Extension for Blog {
|
/// impl Extension for Blog {
|
||||||
/// fn name(&self) -> L10n { L10n::n("Blog") }
|
/// fn name(&self) -> L10n {
|
||||||
/// fn description(&self) -> L10n { L10n::n("Blog system") }
|
/// L10n::n("Blog")
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn description(&self) -> L10n {
|
||||||
|
/// L10n::n("Blog system")
|
||||||
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Extension: AnyInfo + Send + Sync {
|
pub trait Extension: AnyInfo + Send + Sync {
|
||||||
|
|
@ -34,14 +33,19 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Descripción corta localizada de la extensión para paneles, listados, etc.
|
/// Descripción corta localizada de la extensión para paneles, listados, etc.
|
||||||
|
///
|
||||||
|
/// Por defecto devuelve un valor vacío (`L10n::default()`).
|
||||||
fn description(&self) -> L10n {
|
fn description(&self) -> L10n {
|
||||||
L10n::default()
|
L10n::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve una referencia a esta misma extensión cuando se trata de un tema.
|
/// Devuelve una referencia a esta misma extensión cuando actúa como un tema.
|
||||||
///
|
///
|
||||||
/// Para ello, debe implementar [`Extension`] y también [`Theme`](crate::core::theme::Theme). Si
|
/// Para ello, la implementación concreta debe ser una extensión que también implemente
|
||||||
/// la extensión no es un tema, este método devuelve `None` por defecto.
|
/// [`Theme`](crate::core::theme::Theme). Por defecto, asume que la extensión no es un tema y
|
||||||
|
/// devuelve `None`.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
|
@ -61,17 +65,17 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
|
|
||||||
/// Otras extensiones que deben habilitarse **antes** de esta.
|
/// Otras extensiones que deben habilitarse **antes** de esta.
|
||||||
///
|
///
|
||||||
/// PageTop las resolverá automáticamente respetando el orden durante el arranque de la
|
/// PageTop resolverá automáticamente estas dependencias respetando el orden durante el arranque
|
||||||
/// aplicación.
|
/// de la aplicación.
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve la lista de acciones que la extensión va a registrar.
|
/// Devuelve la lista de acciones que la extensión registra.
|
||||||
///
|
///
|
||||||
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
|
/// Estas [acciones](crate::core::action) se despachan por orden de registro o por
|
||||||
/// [peso](crate::Weight), permitiendo personalizar el comportamiento de la aplicación en puntos
|
/// [peso](crate::Weight) (ver [`actions_boxed!`](crate::actions_boxed)), permitiendo
|
||||||
/// específicos.
|
/// personalizar el comportamiento de la aplicación en puntos específicos.
|
||||||
fn actions(&self) -> Vec<ActionBox> {
|
fn actions(&self) -> Vec<ActionBox> {
|
||||||
actions_boxed![]
|
actions_boxed![]
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +89,8 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
/// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros
|
/// Configura los servicios web de la extensión, como rutas, *middleware*, acceso a ficheros
|
||||||
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
|
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
|
||||||
///
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// pub struct ExtensionSample;
|
/// pub struct ExtensionSample;
|
||||||
|
|
@ -98,11 +104,15 @@ pub trait Extension: AnyInfo + Send + Sync {
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
||||||
|
|
||||||
/// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
|
/// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras
|
||||||
/// anteriores de la aplicación.
|
/// extensiones asociadas a versiones anteriores de la aplicación.
|
||||||
///
|
///
|
||||||
/// Actualmente no se usa, pero se deja como *placeholder* para futuras implementaciones.
|
/// Actualmente PageTop no utiliza este método, pero se reserva como *placeholder* para futuras
|
||||||
|
/// implementaciones.
|
||||||
fn drop_extensions(&self) -> Vec<ExtensionRef> {
|
fn drop_extensions(&self) -> Vec<ExtensionRef> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Representa una referencia a una extensión.
|
||||||
|
pub type ExtensionRef = &'static dyn Extension;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,206 @@
|
||||||
//! API para añadir y gestionar nuevos temas.
|
//! API para añadir y gestionar nuevos temas.
|
||||||
//!
|
//!
|
||||||
//! En PageTop un tema es la *piel* de la aplicación. Es responsable último de los estilos,
|
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y
|
||||||
//! tipografías, espaciados y cualquier otro detalle visual o interactivo (animaciones, scripts de
|
//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme)
|
||||||
//! interfaz, etc.).
|
//! permita identificar y registrar los temas disponibles.
|
||||||
//!
|
//!
|
||||||
//! Un tema determina el aspecto final de un documento HTML sin alterar la lógica interna de los
|
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
|
||||||
//! componentes ni la estructura del documento, que queda definida por la plantilla
|
//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo
|
||||||
//! ([`Template`](crate::base::component::Template)) utilizada por cada página.
|
//! del documento a partir de varias regiones ([`Region`]). Cada región es un contenedor lógico
|
||||||
|
//! identificado por un nombre, cuyo contenido se obtiene del [`Context`] de la página.
|
||||||
//!
|
//!
|
||||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension), por
|
//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo.
|
||||||
//! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión.
|
//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio
|
||||||
//! También deben implementar [`Theme`] y sobrescribir el método
|
//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
|
||||||
//! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda
|
//! asociados a cada región.
|
||||||
//! registrarlos como temas.
|
//!
|
||||||
|
//! De este modo, temas y extensiones colaboran sobre una estructura común: las aplicaciones
|
||||||
|
//! registran componentes en el [`Context`], las plantillas organizan las regiones y las páginas
|
||||||
|
//! generan el documento HTML resultante.
|
||||||
|
//!
|
||||||
|
//! Los temas pueden definir sus propias implementaciones de [`Template`] y [`Region`] (por ejemplo,
|
||||||
|
//! mediante *enums* adicionales) para añadir nuevas plantillas o exponer regiones específicas.
|
||||||
|
|
||||||
|
use crate::core::component::Context;
|
||||||
|
use crate::html::{html, Markup};
|
||||||
|
use crate::locale::L10n;
|
||||||
|
use crate::{join, AutoDefault};
|
||||||
|
|
||||||
|
// **< Region >*************************************************************************************
|
||||||
|
|
||||||
|
/// Interfaz común para las regiones lógicas de un documento.
|
||||||
|
///
|
||||||
|
/// Una `Region` representa un contenedor lógico identificado por un nombre de región. Su contenido
|
||||||
|
/// se obtiene del [`Context`], donde los componentes suelen registrarse usando implementaciones de
|
||||||
|
/// métodos como [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in).
|
||||||
|
///
|
||||||
|
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
|
||||||
|
/// implementaciones de [`Region`] que devuelvan el mismo nombre compartirán el mismo conjunto de
|
||||||
|
/// componentes registrados en el [`Context`], aunque cada región puede renderizar ese contenido de
|
||||||
|
/// forma diferente. Por ejemplo, [`DefaultRegion::Header`] y `BootsierRegion::Header` mostrarían
|
||||||
|
/// los mismos componentes si ambas devuelven el nombre `"header"`, pero podrían maquetarse de
|
||||||
|
/// manera distinta.
|
||||||
|
///
|
||||||
|
/// El tema decide qué regiones mostrar en el cuerpo del documento, normalmente usando una plantilla
|
||||||
|
/// ([`Template`]) al renderizar la página ([`Page`](crate::response::page::Page)).
|
||||||
|
pub trait Region {
|
||||||
|
/// Devuelve el nombre de la región.
|
||||||
|
///
|
||||||
|
/// Este nombre es el identificador lógico de la región y se usa como clave en el [`Context`]
|
||||||
|
/// para recuperar y renderizar el contenido registrado bajo ese nombre. Cualquier
|
||||||
|
/// implementación de [`Region`] que devuelva el mismo nombre compartirá el mismo conjunto de
|
||||||
|
/// componentes.
|
||||||
|
///
|
||||||
|
/// En la implementación predeterminada de [`Self::render()`] también se utiliza para construir
|
||||||
|
/// las clases del contenedor de la región (`"region region-<name>"`).
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Devuelve la etiqueta de accesibilidad localizada asociada a la región.
|
||||||
|
///
|
||||||
|
/// En la implementación predeterminada de [`Self::render()`], este valor se usa como
|
||||||
|
/// `aria-label` del contenedor de la región.
|
||||||
|
fn label(&self) -> L10n;
|
||||||
|
|
||||||
|
/// Renderiza el contenedor de la región.
|
||||||
|
///
|
||||||
|
/// Por defecto, recupera del [`Context`] el contenido de la región y, si no está vacío, lo
|
||||||
|
/// envuelve en un `<div>` con clases `"region region-<name>"` y un `aria-label` basado en la
|
||||||
|
/// etiqueta localizada de la región:
|
||||||
|
///
|
||||||
|
/// ```html
|
||||||
|
/// <div class="region region-<name>" role="region" aria-label="<label>">
|
||||||
|
/// <!-- Componentes de la región "name" -->
|
||||||
|
/// </div>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Se puede sobrescribir este método para modificar la estructura del contenedor, las clases
|
||||||
|
/// utilizadas o la semántica del marcado generado para cada región.
|
||||||
|
fn render(&'static self, cx: &mut Context) -> Markup
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
html! {
|
||||||
|
@let region = cx.render_region(self);
|
||||||
|
@if !region.is_empty() {
|
||||||
|
div
|
||||||
|
class=(join!("region region-", self.name()))
|
||||||
|
role="region"
|
||||||
|
aria-label=[self.label().lookup(cx)]
|
||||||
|
{
|
||||||
|
(region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referencia estática a una región.
|
||||||
|
pub type RegionRef = &'static dyn Region;
|
||||||
|
|
||||||
|
// **< DefaultRegion >******************************************************************************
|
||||||
|
|
||||||
|
/// Regiones básicas que PageTop proporciona por defecto.
|
||||||
|
///
|
||||||
|
/// Estas regiones comparten sus nombres (`"header"`, `"content"`, `"footer"`) con cualquier región
|
||||||
|
/// equivalente definida por otros temas, por lo que comparten también el contenido registrado bajo
|
||||||
|
/// esos nombres.
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum DefaultRegion {
|
||||||
|
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
|
||||||
|
///
|
||||||
|
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
||||||
|
Header,
|
||||||
|
|
||||||
|
/// Región principal de **contenido**, de nombre `"content"`.
|
||||||
|
///
|
||||||
|
/// Es la región donde se renderiza el contenido principal del documento. En general será la
|
||||||
|
/// región mínima imprescindible para que una página tenga sentido.
|
||||||
|
#[default]
|
||||||
|
Content,
|
||||||
|
|
||||||
|
/// Región estándar para el **pie de página**, de nombre `"footer"`.
|
||||||
|
///
|
||||||
|
/// Suele contener información legal, enlaces secundarios, créditos, etc.
|
||||||
|
Footer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Region for DefaultRegion {
|
||||||
|
#[inline]
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Header => "header",
|
||||||
|
Self::Content => "content",
|
||||||
|
Self::Footer => "footer",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn label(&self) -> L10n {
|
||||||
|
match self {
|
||||||
|
Self::Header => L10n::l("region-header"),
|
||||||
|
Self::Content => L10n::l("region-content"),
|
||||||
|
Self::Footer => L10n::l("region-footer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Template >***********************************************************************************
|
||||||
|
|
||||||
|
/// Interfaz común para definir plantillas de contenido.
|
||||||
|
///
|
||||||
|
/// Una `Template` puede proporcionar una o más variantes para decidir la composición del `<body>`
|
||||||
|
/// de una página ([`Page`](crate::response::page::Page)). El tema utiliza esta información para
|
||||||
|
/// determinar qué regiones ([`Region`]) deben renderizarse y en qué orden.
|
||||||
|
pub trait Template {
|
||||||
|
/// Renderiza el contenido de la plantilla.
|
||||||
|
///
|
||||||
|
/// Por defecto, renderiza las regiones básicas de [`DefaultRegion`] en este orden:
|
||||||
|
/// [`DefaultRegion::Header`], [`DefaultRegion::Content`] y [`DefaultRegion::Footer`].
|
||||||
|
///
|
||||||
|
/// Se puede sobrescribir este método para:
|
||||||
|
///
|
||||||
|
/// - Cambiar el conjunto de regiones que se renderizan según variantes de la plantilla.
|
||||||
|
/// - Alterar el orden de dichas regiones.
|
||||||
|
/// - Envolver las regiones en contenedores adicionales.
|
||||||
|
/// - Implementar distribuciones específicas (por ejemplo, con barras laterales).
|
||||||
|
///
|
||||||
|
/// Este método se invoca normalmente desde [`Theme::render_page_body()`] para generar el
|
||||||
|
/// contenido del `<body>` de una página según la plantilla devuelta por el contexto de la
|
||||||
|
/// propia página ([`Contextual::template()`](crate::core::component::Contextual::template())).
|
||||||
|
fn render(&'static self, cx: &mut Context) -> Markup {
|
||||||
|
html! {
|
||||||
|
(DefaultRegion::Header.render(cx))
|
||||||
|
(DefaultRegion::Content.render(cx))
|
||||||
|
(DefaultRegion::Footer.render(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referencia estática a una plantilla.
|
||||||
|
pub type TemplateRef = &'static dyn Template;
|
||||||
|
|
||||||
|
// **< DefaultTemplate >****************************************************************************
|
||||||
|
|
||||||
|
/// Plantillas que PageTop proporciona por defecto.
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum DefaultTemplate {
|
||||||
|
/// Plantilla predeterminada.
|
||||||
|
///
|
||||||
|
/// Utiliza la implementación por defecto de [`Template::render()`] y se emplea cuando no se
|
||||||
|
/// selecciona ninguna otra plantilla explícitamente.
|
||||||
|
#[default]
|
||||||
|
Standard,
|
||||||
|
|
||||||
|
/// Plantilla de error.
|
||||||
|
///
|
||||||
|
/// Se utiliza para páginas de error u otros estados excepcionales. Por defecto utiliza la misma
|
||||||
|
/// implementación de [`Template::render()`] que [`Self::Standard`].
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Template for DefaultTemplate {}
|
||||||
|
|
||||||
|
// **< Definitions >********************************************************************************
|
||||||
|
|
||||||
mod definition;
|
mod definition;
|
||||||
pub use definition::{Theme, ThemeRef};
|
pub use definition::{Theme, ThemeRef};
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,26 @@
|
||||||
use crate::base::component::Template;
|
use crate::core::component::Contextual;
|
||||||
use crate::core::component::{ComponentRender, ContextOp, Contextual};
|
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
use crate::global;
|
use crate::global;
|
||||||
use crate::html::{html, Markup, StyleSheet};
|
use crate::html::{html, Markup};
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
|
use crate::prelude::{DefaultTemplate, TemplateRef};
|
||||||
use crate::response::page::Page;
|
use crate::response::page::Page;
|
||||||
|
|
||||||
/// Referencia estática a un tema.
|
|
||||||
///
|
|
||||||
/// Los temas son también extensiones. Por tanto, deben declararse como **instancias estáticas** que
|
|
||||||
/// implementen [`Theme`] y, a su vez, [`Extension`]. Estas instancias se exponen usando
|
|
||||||
/// [`Extension::theme()`](crate::core::extension::Extension::theme).
|
|
||||||
pub type ThemeRef = &'static dyn Theme;
|
|
||||||
|
|
||||||
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
/// Interfaz común que debe implementar cualquier tema de PageTop.
|
||||||
///
|
///
|
||||||
/// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de
|
/// Un tema es una [`Extension`](crate::core::extension::Extension) que define el aspecto general de
|
||||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` mediante plantillas
|
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` usando plantillas
|
||||||
/// ([`Template`]) y qué contenido mostrar en las páginas de error.
|
/// ([`Template`](crate::core::theme::Template)) que maquetan regiones
|
||||||
|
/// ([`Region`](crate::core::theme::Region)) y qué contenido mostrar en las páginas de error. El
|
||||||
|
/// contenido de cada región depende del [`Context`](crate::core::component::Context) y de su nombre
|
||||||
|
/// lógico.
|
||||||
///
|
///
|
||||||
/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden
|
/// Todos los métodos de este *trait* tienen una implementación por defecto, por lo que pueden
|
||||||
/// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los
|
/// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los
|
||||||
/// predeterminados.
|
/// predeterminados.
|
||||||
///
|
///
|
||||||
/// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme),
|
/// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme),
|
||||||
/// que debe devolver una referencia estática al propio tema:
|
/// que debe devolver una referencia al propio tema:
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
|
|
@ -47,32 +43,55 @@ pub type ThemeRef = &'static dyn Theme;
|
||||||
/// impl Theme for MyTheme {}
|
/// impl Theme for MyTheme {}
|
||||||
/// ```
|
/// ```
|
||||||
pub trait Theme: Extension + Send + Sync {
|
pub trait Theme: Extension + Send + Sync {
|
||||||
|
/// Devuelve la plantilla ([`Template`](crate::core::theme::Template)) que el propio tema
|
||||||
|
/// propone como predeterminada.
|
||||||
|
///
|
||||||
|
/// Se utiliza al inicializar un [`Context`](crate::core::component::Context) o una página
|
||||||
|
/// ([`Page`](crate::response::page::Page)) por si no se elige ninguna otra plantilla con
|
||||||
|
/// [`Contextual::with_template()`](crate::core::component::Contextual::with_template).
|
||||||
|
///
|
||||||
|
/// La implementación por defecto devuelve la plantilla estándar ([`DefaultTemplate::Standard`])
|
||||||
|
/// con una estructura básica para la página. Los temas pueden sobrescribir este método para
|
||||||
|
/// seleccionar otra plantilla predeterminada o una plantilla propia.
|
||||||
|
#[inline]
|
||||||
|
fn default_template(&self) -> TemplateRef {
|
||||||
|
&DefaultTemplate::Standard
|
||||||
|
}
|
||||||
|
|
||||||
/// Acciones específicas del tema antes de renderizar el `<body>` de la página.
|
/// Acciones específicas del tema antes de renderizar el `<body>` de la página.
|
||||||
///
|
///
|
||||||
/// Se invoca antes de que se procese la plantilla ([`Template`]) asociada a la página
|
/// Es un buen lugar para inicializar o ajustar recursos en función del contexto de la página,
|
||||||
/// ([`Page::template()`](crate::response::page::Page::template)). Es un buen lugar para
|
/// por ejemplo:
|
||||||
/// inicializar o ajustar recursos en función del contexto de la página, por ejemplo:
|
|
||||||
///
|
///
|
||||||
/// - Añadir metadatos o propiedades a la página.
|
/// - Añadir metadatos o propiedades a la cabecera de la página.
|
||||||
/// - Preparar atributos compartidos.
|
/// - Preparar atributos compartidos.
|
||||||
/// - Registrar *assets* condicionales en el contexto.
|
/// - Registrar *assets* condicionales en el contexto.
|
||||||
|
///
|
||||||
|
/// La implementación por defecto no realiza ninguna acción.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn before_render_page_body(&self, page: &mut Page) {}
|
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, delega en la plantilla ([`Template`]) asociada a la página
|
/// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde
|
||||||
/// ([`Page::template()`](crate::response::page::Page::template)). La plantilla se encarga de
|
/// su [`Context`](crate::core::component::Context), y llama a
|
||||||
/// procesar las regiones y renderizar los componentes registrados en el contexto.
|
/// [`Template::render()`](crate::core::theme::Template::render) para componer el `<body>` a
|
||||||
|
/// partir de las regiones.
|
||||||
|
///
|
||||||
|
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
||||||
|
/// [`DefaultRegion::Header`](crate::core::theme::DefaultRegion::Header),
|
||||||
|
/// [`DefaultRegion::Content`](crate::core::theme::DefaultRegion::Content) y
|
||||||
|
/// [`DefaultRegion::Footer`](crate::core::theme::DefaultRegion::Footer) en ese orden.
|
||||||
///
|
///
|
||||||
/// Los temas pueden sobrescribir este método para:
|
/// Los temas pueden sobrescribir este método para:
|
||||||
///
|
///
|
||||||
/// - Forzar una plantilla concreta en determinadas páginas.
|
/// - Forzar una plantilla concreta en determinadas páginas.
|
||||||
/// - Envolver el contenido en marcadores adicionales.
|
/// - Consultar la plantilla de la página y variar la composición según su nombre.
|
||||||
|
/// - Envolver el contenido en contenedores adicionales.
|
||||||
/// - Implementar lógicas de composición alternativas.
|
/// - Implementar lógicas de composición alternativas.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn render_page_body(&self, page: &mut Page) -> Markup {
|
fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||||
Template::named(page.template()).render(page.context())
|
page.template().render(page.context())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acciones específicas del tema después de renderizar el `<body>` de la página.
|
/// Acciones específicas del tema después de renderizar el `<body>` de la página.
|
||||||
|
|
@ -83,6 +102,8 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// - Realizar *tracing* o recopilar métricas.
|
/// - Realizar *tracing* o recopilar métricas.
|
||||||
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
|
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
|
||||||
/// respuesta final.
|
/// respuesta final.
|
||||||
|
///
|
||||||
|
/// La implementación por defecto no realiza ninguna acción.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn after_render_page_body(&self, page: &mut Page) {}
|
fn after_render_page_body(&self, page: &mut Page) {}
|
||||||
|
|
||||||
|
|
@ -101,34 +122,11 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// - La etiqueta `viewport` básica para diseño adaptable.
|
/// - La etiqueta `viewport` básica para diseño adaptable.
|
||||||
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
|
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
|
||||||
/// página.
|
/// página.
|
||||||
/// - Los *assets* registrados en el contexto de la página. Si el parámetro
|
/// - Los *assets* registrados en el contexto de la página.
|
||||||
/// `include_basic_assets` está activado, añade de serie las siguientes hojas de estilo
|
|
||||||
/// básicas: `normalize.css`, `root.css`, `basic.css`, útiles para temas sencillos o de uso
|
|
||||||
/// general.
|
|
||||||
///
|
///
|
||||||
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
||||||
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
||||||
#[inline]
|
|
||||||
fn render_page_head(&self, page: &mut Page) -> Markup {
|
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),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
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! {
|
||||||
meta charset="utf-8";
|
meta charset="utf-8";
|
||||||
|
|
@ -173,3 +171,6 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
|
html! { div { h1 { (L10n::l("error404_notice").using(page)) } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Referencia estática a un tema.
|
||||||
|
pub type ThemeRef = &'static dyn Theme;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::base::component::Region;
|
|
||||||
use crate::core::component::{Child, ChildOp, Children};
|
use crate::core::component::{Child, ChildOp, Children};
|
||||||
use crate::core::theme::ThemeRef;
|
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
@ -21,24 +20,23 @@ static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
||||||
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
||||||
|
|
||||||
impl ChildrenInRegions {
|
impl ChildrenInRegions {
|
||||||
pub fn with(region_name: impl AsRef<str>, child: Child) -> Self {
|
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
||||||
Self::default().with_child_in(region_name, ChildOp::Add(child))
|
Self::default().with_child_in(region_ref, ChildOp::Add(child))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
pub fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||||
let name = region_name.as_ref();
|
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
||||||
if let Some(region) = self.0.get_mut(name) {
|
|
||||||
region.alter_child(op);
|
region.alter_child(op);
|
||||||
} else {
|
} else {
|
||||||
self.0
|
self.0
|
||||||
.insert(name.to_owned(), Children::new().with_child(op));
|
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef<str>) -> Children {
|
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
||||||
let name = region_name.as_ref();
|
let name = region_ref.name();
|
||||||
let common = COMMON_REGIONS.read();
|
let common = COMMON_REGIONS.read();
|
||||||
let themed = THEME_REGIONS.read();
|
let themed = THEME_REGIONS.read();
|
||||||
|
|
||||||
|
|
@ -50,20 +48,36 @@ impl ChildrenInRegions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Permite añadir componentes a regiones globales o específicas de un tema.
|
/// Añade componentes a regiones globales o específicas de un tema.
|
||||||
///
|
///
|
||||||
/// Según la variante, se pueden añadir componentes ([`add()`](Self::add)) que permanecerán
|
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
|
||||||
/// disponibles durante toda la ejecución.
|
/// componentes añadidos se mantienen durante toda la ejecución y se inyectan automáticamente al
|
||||||
///
|
/// renderizar los documentos HTML que utilizan esas regiones, como las páginas de contenido
|
||||||
/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
|
/// ([`Page`](crate::response::page::Page)).
|
||||||
/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
|
|
||||||
pub enum InRegion {
|
pub enum InRegion {
|
||||||
/// Región de contenido por defecto.
|
/// Región principal de **contenido** por defecto.
|
||||||
Default,
|
///
|
||||||
/// Región identificada por el nombre proporcionado.
|
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
|
||||||
Named(&'static str),
|
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
|
||||||
/// Región identificada por su nombre para un tema concreto.
|
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
|
||||||
OfTheme(&'static str, ThemeRef),
|
/// directamente [`DefaultRegion::Content`] o cualquier otra implementación de
|
||||||
|
/// [`Region`](crate::core::theme::Region) que devuelva ese mismo nombre, mostrará los
|
||||||
|
/// componentes registrados aquí, aunque lo harán según su propio método de renderizado
|
||||||
|
/// ([`Region::render()`](crate::core::theme::Region::render)).
|
||||||
|
Content,
|
||||||
|
/// Región global compartida por todos los temas.
|
||||||
|
///
|
||||||
|
/// Los componentes añadidos aquí se asocian al nombre de la región indicado por [`RegionRef`],
|
||||||
|
/// es decir, al valor devuelto por [`Region::name()`](crate::core::theme::Region::name) para
|
||||||
|
/// esa región. Se mostrarán en cualquier tema cuya plantilla renderice una región que devuelva
|
||||||
|
/// ese mismo nombre.
|
||||||
|
Global(RegionRef),
|
||||||
|
/// Región asociada a un tema concreto.
|
||||||
|
///
|
||||||
|
/// Los componentes sólo se renderizarán cuando el documento se procese con el tema indicado y
|
||||||
|
/// se utilice la región referenciada. Resulta útil para añadir contenido específico en un tema
|
||||||
|
/// sin afectar a otros.
|
||||||
|
ForTheme(ThemeRef, RegionRef),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InRegion {
|
impl InRegion {
|
||||||
|
|
@ -73,28 +87,33 @@ impl InRegion {
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// // Banner global, en la región por defecto de cualquier página.
|
/// // Banner global en la región por defecto.
|
||||||
/// InRegion::Default.add(Child::with(Html::with(|_|
|
/// InRegion::Content.add(Child::with(Html::with(|_| {
|
||||||
/// html! { ("🎉 ¡Bienvenido!") }
|
/// html! { "🎉 ¡Bienvenido!" }
|
||||||
/// )));
|
/// })));
|
||||||
///
|
///
|
||||||
/// // Texto en la región "sidebar".
|
/// // Texto en la cabecera.
|
||||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
|
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
|
||||||
/// html! { ("Publicidad") }
|
/// html! { "Publicidad" }
|
||||||
/// )));
|
/// })));
|
||||||
|
///
|
||||||
|
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||||
|
/// InRegion::ForTheme(&theme::Basic, &DefaultRegion::Footer).add(Child::with(Html::with(|_| {
|
||||||
|
/// html! { "Aviso legal" }
|
||||||
|
/// })));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add(&self, child: Child) -> &Self {
|
pub fn add(&self, child: Child) -> &Self {
|
||||||
match self {
|
match self {
|
||||||
InRegion::Default => Self::add_to_common(Region::DEFAULT, child),
|
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, child),
|
||||||
InRegion::Named(region_name) => Self::add_to_common(region_name, child),
|
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
|
||||||
InRegion::OfTheme(region_name, theme_ref) => {
|
InRegion::ForTheme(theme_ref, region_ref) => {
|
||||||
let mut regions = THEME_REGIONS.write();
|
let mut regions = THEME_REGIONS.write();
|
||||||
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
if let Some(r) = regions.get_mut(&theme_ref.type_id()) {
|
||||||
r.alter_child_in(region_name, ChildOp::Add(child));
|
r.alter_child_in(*region_ref, ChildOp::Add(child));
|
||||||
} else {
|
} else {
|
||||||
regions.insert(
|
regions.insert(
|
||||||
theme_ref.type_id(),
|
theme_ref.type_id(),
|
||||||
ChildrenInRegions::with(region_name, child),
|
ChildrenInRegions::with(*region_ref, child),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,9 +122,9 @@ impl InRegion {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn add_to_common(region_name: &str, child: Child) {
|
fn add_to_common(region_ref: RegionRef, child: Child) {
|
||||||
COMMON_REGIONS
|
COMMON_REGIONS
|
||||||
.write()
|
.write()
|
||||||
.alter_child_in(region_name, ChildOp::Add(child));
|
.alter_child_in(region_ref, ChildOp::Add(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,27 @@
|
||||||
|
//! Responde a una petición web generando una página HTML completa.
|
||||||
|
//!
|
||||||
|
//! Este módulo define [`Page`], que representa una página HTML lista para renderizar. Cada página
|
||||||
|
//! se construye a partir de un [`Context`] propio, donde se registran el tema activo, la plantilla
|
||||||
|
//! ([`Template`](crate::core::theme::Template)) que define la disposición de las regiones
|
||||||
|
//! ([`Region`]), los componentes asociados y los recursos adicionales (hojas de estilo, scripts,
|
||||||
|
//! *favicon*, etc.).
|
||||||
|
//!
|
||||||
|
//! El renderizado ([`Page::render()`]) delega en el tema ([`Theme`](crate::core::theme::Theme)) la
|
||||||
|
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
|
||||||
|
//! extensiones antes y después de generar los contenidos.
|
||||||
|
//!
|
||||||
|
//! También introduce regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de
|
||||||
|
//! anclaje globales al inicio y al final del documento.
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
pub use error::ErrorPage;
|
pub use error::ErrorPage;
|
||||||
|
|
||||||
pub use actix_web::Result as ResultPage;
|
pub use actix_web::Result as ResultPage;
|
||||||
|
|
||||||
use crate::base::action;
|
use crate::base::action;
|
||||||
use crate::base::component::Region;
|
use crate::core::component::{Child, ChildOp, Component};
|
||||||
use crate::core::component::{Child, ChildOp, Component, ComponentRender};
|
|
||||||
use crate::core::component::{Context, ContextOp, Contextual};
|
use crate::core::component::{Context, ContextOp, Contextual};
|
||||||
use crate::core::theme::ThemeRef;
|
use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{html, Markup, DOCTYPE};
|
use crate::html::{html, Markup, DOCTYPE};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
use crate::html::{AttrClasses, ClassesOp};
|
use crate::html::{AttrClasses, ClassesOp};
|
||||||
|
|
@ -16,6 +30,57 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
||||||
use crate::service::HttpRequest;
|
use crate::service::HttpRequest;
|
||||||
use crate::{builder_fn, AutoDefault};
|
use crate::{builder_fn, AutoDefault};
|
||||||
|
|
||||||
|
// **< ReservedRegion >*****************************************************************************
|
||||||
|
|
||||||
|
/// Regiones internas reservadas como puntos de anclaje globales.
|
||||||
|
///
|
||||||
|
/// Representan contenedores especiales situados al inicio y al final de un documento. Están
|
||||||
|
/// pensadas para proporcionar regiones donde inyectar contenido global o técnico. No suelen usarse
|
||||||
|
/// como regiones visibles en los temas.
|
||||||
|
pub enum ReservedRegion {
|
||||||
|
/// Región interna situada al **inicio del documento**.
|
||||||
|
///
|
||||||
|
/// Su función es proporcionar un contenedor donde las extensiones puedan inyectar contenido
|
||||||
|
/// global antes del resto de regiones principales (cabecera, contenido, etc.).
|
||||||
|
///
|
||||||
|
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
||||||
|
/// sino como punto de anclaje para elementos auxiliares, marcadores técnicos, inicializadores o
|
||||||
|
/// contenido de depuración que deban situarse en la parte superior del documento.
|
||||||
|
///
|
||||||
|
/// Se considera una región **reservada** para este tipo de usos globales.
|
||||||
|
PageTop,
|
||||||
|
|
||||||
|
/// Región interna situada al **final del documento**.
|
||||||
|
///
|
||||||
|
/// Pensada para proporcionar un contenedor donde las extensiones puedan inyectar contenido
|
||||||
|
/// global después del resto de regiones principales (cabecera, contenido, etc.).
|
||||||
|
///
|
||||||
|
/// No suele utilizarse en los temas como una región “visible” dentro del maquetado habitual,
|
||||||
|
/// sino como punto de anclaje para elementos auxiliares asociados a comportamientos dinámicos
|
||||||
|
/// que deban situarse en la parte inferior del documento.
|
||||||
|
///
|
||||||
|
/// Igual que [`Self::PageTop`], se considera una región **reservada** para este tipo de usos
|
||||||
|
/// globales.
|
||||||
|
PageBottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Region for ReservedRegion {
|
||||||
|
#[inline]
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::PageTop => "page-top",
|
||||||
|
Self::PageBottom => "page-bottom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn label(&self) -> L10n {
|
||||||
|
L10n::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Page >***************************************************************************************
|
||||||
|
|
||||||
/// Representa una página HTML completa lista para renderizar.
|
/// Representa una página HTML completa lista para renderizar.
|
||||||
///
|
///
|
||||||
/// Una instancia de `Page` se compone dinámicamente permitiendo establecer título, descripción,
|
/// Una instancia de `Page` se compone dinámicamente permitiendo establecer título, descripción,
|
||||||
|
|
@ -77,7 +142,7 @@ impl Page {
|
||||||
/// Añade una entrada `<meta property="..." content="...">` al `<head>`.
|
/// Añade una entrada `<meta property="..." content="...">` al `<head>`.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self {
|
pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self {
|
||||||
self.metadata.push((property, content));
|
self.properties.push((property, content));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,15 +162,17 @@ impl Page {
|
||||||
|
|
||||||
/// Añade un componente hijo a la región de contenido por defecto.
|
/// Añade un componente hijo a la región de contenido por defecto.
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
pub fn add_child(mut self, component: impl Component) -> Self {
|
||||||
self.context
|
self.context.alter_child_in(
|
||||||
.alter_child_in(Region::DEFAULT, ChildOp::Add(Child::with(component)));
|
&DefaultRegion::Content,
|
||||||
|
ChildOp::Add(Child::with(component)),
|
||||||
|
);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Añade un componente hijo en la región `region_name` de la página.
|
/// Añade un componente hijo en la región `region_name` de la página.
|
||||||
pub fn add_child_in(mut self, region_name: &'static str, component: impl Component) -> Self {
|
pub fn add_child_in(mut self, region_ref: RegionRef, component: impl Component) -> Self {
|
||||||
self.context
|
self.context
|
||||||
.alter_child_in(region_name, ChildOp::Add(Child::with(component)));
|
.alter_child_in(region_ref, ChildOp::Add(Child::with(component)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,8 +221,30 @@ impl Page {
|
||||||
|
|
||||||
/// Renderiza la página completa en formato HTML.
|
/// Renderiza la página completa en formato HTML.
|
||||||
///
|
///
|
||||||
/// Ejecuta las acciones correspondientes antes y después de renderizar el `<body>`,
|
/// El proceso de renderizado de la página sigue esta secuencia:
|
||||||
/// así como del `<head>`, e inserta los atributos `lang` y `dir` en la etiqueta `<html>`.
|
///
|
||||||
|
/// 1. Ejecuta
|
||||||
|
/// [`Theme::before_render_page_body()`](crate::core::theme::Theme::before_render_page_body)
|
||||||
|
/// para que el tema pueda ejecutar acciones específicas antes de renderizar el `<body>`.
|
||||||
|
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
|
||||||
|
/// ajustes previos sobre la página.
|
||||||
|
/// 3. **Construye el contenido del `<body>`**:
|
||||||
|
/// - Renderiza la región reservada superior ([`ReservedRegion::PageTop`]).
|
||||||
|
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
|
||||||
|
/// renderizar las regiones del cuerpo principal de la página.
|
||||||
|
/// - Renderiza la región reservada inferior ([`ReservedRegion::PageBottom`]).
|
||||||
|
/// 4. Ejecuta
|
||||||
|
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
|
||||||
|
/// para que el tema pueda aplicar ajustes finales.
|
||||||
|
/// 5. Despacha [`action::page::AfterRenderBody`] para permitir que otras extensiones realicen
|
||||||
|
/// sus últimos ajustes tras generar el `<body>`.
|
||||||
|
/// 6. Renderiza el `<head>` llamando a
|
||||||
|
/// [`Theme::render_page_head()`](crate::core::theme::Theme::render_page_head).
|
||||||
|
/// 7. Obtiene el idioma y la dirección del texto a partir de
|
||||||
|
/// [`Context::langid()`](crate::core::component::Context::langid) e inserta los atributos
|
||||||
|
/// `lang` y `dir` en la etiqueta `<html>`.
|
||||||
|
/// 8. Compone el documento HTML completo (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`) y
|
||||||
|
/// devuelve un [`ResultPage`] con el [`Markup`] final.
|
||||||
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
|
pub fn render(&mut self) -> ResultPage<Markup, ErrorPage> {
|
||||||
// Acciones específicas del tema antes de renderizar el <body>.
|
// Acciones específicas del tema antes de renderizar el <body>.
|
||||||
self.context.theme().before_render_page_body(self);
|
self.context.theme().before_render_page_body(self);
|
||||||
|
|
@ -165,9 +254,9 @@ impl Page {
|
||||||
|
|
||||||
// Renderiza el <body>.
|
// Renderiza el <body>.
|
||||||
let body = html! {
|
let body = html! {
|
||||||
(Region::named(Region::PAGETOP).render(&mut self.context))
|
(ReservedRegion::PageTop.render(&mut self.context))
|
||||||
(self.context.theme().render_page_body(self))
|
(self.context.theme().render_page_body(self))
|
||||||
(Region::named(Region::PAGEBOTTOM).render(&mut self.context))
|
(ReservedRegion::PageBottom.render(&mut self.context))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Acciones específicas del tema después de renderizar el <body>.
|
// Acciones específicas del tema después de renderizar el <body>.
|
||||||
|
|
@ -228,8 +317,8 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_template(mut self, template_name: &'static str) -> Self {
|
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||||
self.context.alter_template(template_name);
|
self.context.alter_template(template);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,8 +335,8 @@ impl Contextual for Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||||
self.context.alter_child_in(region_name, op);
|
self.context.alter_child_in(region_ref, op);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +350,7 @@ impl Contextual for Page {
|
||||||
self.context.theme()
|
self.context.theme()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn template(&self) -> &str {
|
fn template(&self) -> TemplateRef {
|
||||||
self.context.template()
|
self.context.template()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::base::component::{Html, Template};
|
use crate::base::component::Html;
|
||||||
use crate::core::component::Contextual;
|
use crate::core::component::Contextual;
|
||||||
|
use crate::core::theme::DefaultTemplate;
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
use crate::response::ResponseError;
|
use crate::response::ResponseError;
|
||||||
use crate::service::http::{header::ContentType, StatusCode};
|
use crate::service::http::{header::ContentType, StatusCode};
|
||||||
|
|
@ -7,8 +8,21 @@ use crate::service::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use super::Page;
|
use super::Page;
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Página de error asociada a un código de estado HTTP.
|
||||||
|
///
|
||||||
|
/// Este enumerado agrupa los distintos tipos de error que pueden devolverse como página HTML
|
||||||
|
/// completa. Cada variante encapsula la solicitud original ([`HttpRequest`]) y se corresponde con
|
||||||
|
/// un código de estado concreto.
|
||||||
|
///
|
||||||
|
/// Para algunos errores (como [`ErrorPage::AccessDenied`] y [`ErrorPage::NotFound`]) se construye
|
||||||
|
/// una [`Page`] usando la plantilla de error del tema activo ([`DefaultTemplate::Error`]), lo que
|
||||||
|
/// permite personalizar el contenido del mensaje. En el resto de casos se devuelve un cuerpo HTML
|
||||||
|
/// mínimo basado en una descripción genérica del error.
|
||||||
|
///
|
||||||
|
/// `ErrorPage` implementa [`ResponseError`], por lo que puede utilizarse directamente como tipo de
|
||||||
|
/// error en los controladores HTTP.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorPage {
|
pub enum ErrorPage {
|
||||||
NotModified(HttpRequest),
|
NotModified(HttpRequest),
|
||||||
|
|
@ -20,7 +34,7 @@ pub enum ErrorPage {
|
||||||
Timeout(HttpRequest),
|
Timeout(HttpRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ErrorPage {
|
impl fmt::Display for ErrorPage {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
// Error 304.
|
// Error 304.
|
||||||
|
|
@ -33,7 +47,7 @@ impl Display for ErrorPage {
|
||||||
let error403 = error_page.theme().error403(&mut error_page);
|
let error403 = error_page.theme().error403(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error FORBIDDEN"))
|
.with_title(L10n::n("Error FORBIDDEN"))
|
||||||
.with_template(Template::ERROR)
|
.with_template(&DefaultTemplate::Error)
|
||||||
.add_child(Html::with(move |_| error403.clone()))
|
.add_child(Html::with(move |_| error403.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +62,7 @@ impl Display for ErrorPage {
|
||||||
let error404 = error_page.theme().error404(&mut error_page);
|
let error404 = error_page.theme().error404(&mut error_page);
|
||||||
if let Ok(page) = error_page
|
if let Ok(page) = error_page
|
||||||
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
||||||
.with_template(Template::ERROR)
|
.with_template(&DefaultTemplate::Error)
|
||||||
.add_child(Html::with(move |_| error404.clone()))
|
.add_child(Html::with(move |_| error404.clone()))
|
||||||
.render()
|
.render()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,31 @@
|
||||||
|
:root {
|
||||||
|
/* Font families */
|
||||||
|
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif;
|
||||||
|
--val-font-serif: Georgia,"Times New Roman",serif;
|
||||||
|
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||||
|
--val-font-family: var(--val-font-sans);
|
||||||
|
/* Font size */
|
||||||
|
--val-fs--base: 1rem;
|
||||||
|
/* Font weight */
|
||||||
|
--val-fw--base: 400;
|
||||||
|
/* Line height */
|
||||||
|
--val-lh--base: 1.5;
|
||||||
|
/* Colors */
|
||||||
|
--val-color--bg: #fafafa;
|
||||||
|
--val-color--text: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
font-family: var(--val-font-family);
|
font-family: var(--val-font-family);
|
||||||
font-size: var(--val-fs--base);
|
font-size: var(--val-fs--base);
|
||||||
font-weight: var(--val-fw--base);
|
font-weight: var(--val-fw--base);
|
||||||
line-height: var(--val-lh--base);
|
line-height: var(--val-lh--base);
|
||||||
color: var(--val-color--text);
|
color: var(--val-color--text);
|
||||||
background-color: var(--val-color--bg);
|
background-color: var(--val-color--bg);
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/* Icon component */
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* PoweredBy component */
|
|
||||||
|
|
||||||
.poweredby {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
: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;
|
|
||||||
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
|
||||||
--val-font-family: var(--val-font-sans);
|
|
||||||
|
|
||||||
/* Font size */
|
|
||||||
--val-fs--x3l: 2.5rem;
|
|
||||||
--val-fs--x2l: 2rem;
|
|
||||||
--val-fs--xl: 1.75rem;
|
|
||||||
--val-fs--l: 1.5rem;
|
|
||||||
--val-fs--m: 1.25rem;
|
|
||||||
--val-fs--base: 1rem;
|
|
||||||
--val-fs--s: 0.875rem;
|
|
||||||
--val-fs--xs: 0.75rem;
|
|
||||||
--val-fs--x2s: 0.5625rem;
|
|
||||||
--val-fs--x3s: 0.375rem;
|
|
||||||
|
|
||||||
/* Font weight */
|
|
||||||
--val-fw--light: 300;
|
|
||||||
--val-fw--base: 400;
|
|
||||||
--val-fw--bold: 500;
|
|
||||||
|
|
||||||
/* Line height */
|
|
||||||
--val-lh--base: 1.5;
|
|
||||||
--val-lh--header: 1.2;
|
|
||||||
|
|
||||||
--val-max-width: 90rem;
|
|
||||||
/*
|
|
||||||
--val-color-rgb: 33,37,41;
|
|
||||||
--val-main--bg-rgb: 255,255,255;
|
|
||||||
--val-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
|
||||||
|
|
||||||
--line-height-base: 1.6875rem;
|
|
||||||
--line-height-s: 1.125rem;
|
|
||||||
--max-bg-color: 98.125rem;
|
|
||||||
*/
|
|
||||||
--val-gap: 1.125rem;
|
|
||||||
/*
|
|
||||||
--content-left: 5.625rem;
|
|
||||||
--site-header-height-wide: var(--val-gap10);
|
|
||||||
--container-padding: var(--val-gap);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
@media (min-width: 75rem) {
|
|
||||||
:root {
|
|
||||||
--container-padding:var(--val-gap2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--scrollbar-width: 0px;
|
|
||||||
--grid-col-count: 6;
|
|
||||||
--grid-gap: var(--val-gap);
|
|
||||||
--grid-gap-count: calc(var(--grid-col-count) - 1);
|
|
||||||
--grid-full-width: calc(100vw - var(--val-gap2) - var(--scrollbar-width));
|
|
||||||
--grid-col-width: calc((var(--grid-full-width) - (var(--grid-gap-count) * var(--grid-gap))) / var(--grid-col-count));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 43.75rem) {
|
|
||||||
:root {
|
|
||||||
--grid-col-count:14;
|
|
||||||
--grid-gap: var(--val-gap2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 62.5rem) {
|
|
||||||
:root {
|
|
||||||
--scrollbar-width:0.9375rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 75rem) {
|
|
||||||
:root {
|
|
||||||
--grid-full-width:calc(100vw - var(--scrollbar-width) - var(--content-left) - var(--val-gap4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 90rem) {
|
|
||||||
:root {
|
|
||||||
--grid-full-width:calc(var(--max-width) - var(--val-gap4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
:root {
|
|
||||||
--val-gap-0-15: calc(0.15 * var(--val-gap));
|
|
||||||
--val-gap-0-25: calc(0.25 * var(--val-gap));
|
|
||||||
--val-gap-0-35: calc(0.35 * var(--val-gap));
|
|
||||||
--val-gap-0-5: calc(0.5 * var(--val-gap));
|
|
||||||
--val-gap-0-75: calc(0.75 * var(--val-gap));
|
|
||||||
--val-gap-1-5: calc(1.5 * var(--val-gap));
|
|
||||||
--val-gap-2: calc(2 * var(--val-gap));
|
|
||||||
|
|
||||||
--primary-hue: 216;
|
|
||||||
--primary-sat: 60%;
|
|
||||||
--val-color--primary: hsl(var(--primary-hue), var(--primary-sat), 50%);
|
|
||||||
--val-color--primary-light: hsl(var(--primary-hue), var(--primary-sat), 60%);
|
|
||||||
--val-color--primary-dark: hsl(var(--primary-hue), var(--primary-sat), 40%);
|
|
||||||
--val-color--primary-link: hsl(var(--primary-hue), var(--primary-sat), 55%);
|
|
||||||
--val-color--primary-link-hover: hsl(var(--primary-hue), var(--primary-sat), 30%);
|
|
||||||
--val-color--primary-link-active: hsl(var(--primary-hue), var(--primary-sat), 70%);
|
|
||||||
|
|
||||||
--info-hue: 190;
|
|
||||||
--info-sat: 90%;
|
|
||||||
--val-color--info: hsl(var(--info-hue), var(--info-sat), 54%);
|
|
||||||
--val-color--info-light: hsl(var(--info-hue), var(--info-sat), 70%);
|
|
||||||
--val-color--info-dark: hsl(var(--info-hue), var(--info-sat), 45%);
|
|
||||||
--val-color--info-link: hsl(var(--info-hue), var(--info-sat), 30%);
|
|
||||||
--val-color--info-link-hover: hsl(var(--info-hue), var(--info-sat), 20%);
|
|
||||||
--val-color--info-link-active: hsl(var(--info-hue), var(--info-sat), 40%);
|
|
||||||
|
|
||||||
--success-hue: 150;
|
|
||||||
--success-sat: 50%;
|
|
||||||
--val-color--success: hsl(var(--success-hue), var(--success-sat), 50%);
|
|
||||||
--val-color--success-light: hsl(var(--success-hue), var(--success-sat), 68%);
|
|
||||||
--val-color--success-dark: hsl(var(--success-hue), var(--success-sat), 38%);
|
|
||||||
--val-color--success-link: hsl(var(--success-hue), var(--success-sat), 26%);
|
|
||||||
--val-color--success-link-hover: hsl(var(--success-hue), var(--success-sat), 18%);
|
|
||||||
--val-color--success-link-active: hsl(var(--success-hue), var(--success-sat), 36%);
|
|
||||||
|
|
||||||
--warning-hue: 44;
|
|
||||||
--warning-sat: 100%;
|
|
||||||
--val-color--warning: hsl(var(--warning-hue), var(--warning-sat), 50%);
|
|
||||||
--val-color--warning-light: hsl(var(--warning-hue), var(--warning-sat), 60%);
|
|
||||||
--val-color--warning-dark: hsl(var(--warning-hue), var(--warning-sat), 40%);
|
|
||||||
--val-color--warning-link: hsl(var(--warning-hue), var(--warning-sat), 30%);
|
|
||||||
--val-color--warning-link-hover: hsl(var(--warning-hue), var(--warning-sat), 20%);
|
|
||||||
--val-color--warning-link-active: hsl(var(--warning-hue), var(--warning-sat), 38%);
|
|
||||||
|
|
||||||
--danger-hue: 348;
|
|
||||||
--danger-sat: 86%;
|
|
||||||
--val-color--danger: hsl(var(--danger-hue), var(--danger-sat), 50%);
|
|
||||||
--val-color--danger-light: hsl(var(--danger-hue), var(--danger-sat), 60%);
|
|
||||||
--val-color--danger-dark: hsl(var(--danger-hue), var(--danger-sat), 35%);
|
|
||||||
--val-color--danger-link: hsl(var(--danger-hue), var(--danger-sat), 25%);
|
|
||||||
--val-color--danger-link-hover: hsl(var(--danger-hue), var(--danger-sat), 10%);
|
|
||||||
--val-color--danger-link-active: hsl(var(--danger-hue), var(--danger-sat), 30%);
|
|
||||||
|
|
||||||
--light-hue: 0;
|
|
||||||
--light-sat: 0%;
|
|
||||||
--val-color--light: hsl(var(--light-hue), var(--light-sat), 96%);
|
|
||||||
--val-color--light-light: hsl(var(--light-hue), var(--light-sat), 98%);
|
|
||||||
--val-color--light-dark: hsl(var(--light-hue), var(--light-sat), 92%);
|
|
||||||
|
|
||||||
--dark-hue: 0;
|
|
||||||
--dark-sat: 0%;
|
|
||||||
--val-color--dark: hsl(var(--dark-hue), var(--dark-sat), 25%);
|
|
||||||
--val-color--dark-light: hsl(var(--dark-hue), var(--dark-sat), 40%);
|
|
||||||
--val-color--dark-dark: hsl(var(--dark-hue), var(--dark-sat), 8%);
|
|
||||||
--val-color--dark-link: hsl(var(--dark-hue), var(--dark-sat), 90%);
|
|
||||||
--val-color--dark-link-hover: hsl(var(--dark-hue), var(--dark-sat), 100%);
|
|
||||||
--val-color--dark-link-active: hsl(var(--dark-hue), var(--dark-sat), 70%);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--gray-hue: 201;
|
|
||||||
--gray-sat: 15%;
|
|
||||||
--val-color--gray-5: hsl(var(--gray-hue), var(--gray-sat), 5%);
|
|
||||||
--val-color--gray-10: hsl(var(--gray-hue), var(--gray-sat) ,11%);
|
|
||||||
--val-color--gray-20: hsl(var(--gray-hue), var(--gray-sat),20%);
|
|
||||||
--val-color--gray-45: hsl(var(--gray-hue), var(--gray-sat), 44%);
|
|
||||||
--val-color--gray-60: hsl(var(--gray-hue), var(--gray-sat), 57%);
|
|
||||||
--val-color--gray-65: hsl(var(--gray-hue), var(--gray-sat), 63%);
|
|
||||||
--val-color--gray-70: hsl(var(--gray-hue), var(--gray-sat), 72%);
|
|
||||||
--val-color--gray-90: hsl(var(--gray-hue), var(--gray-sat), 88%);
|
|
||||||
--val-color--gray-95: hsl(var(--gray-hue), var(--gray-sat), 93%);
|
|
||||||
--val-color--gray-100: hsl(var(--gray-hue), var(--gray-sat), 97%);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--val-color--bg: #fafafa;
|
|
||||||
--val-color--text: #212529;
|
|
||||||
--val-color--white: #fff;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
--color-text-neutral-soft: var(--color--gray-45);
|
|
||||||
--color-text-neutral-medium: var(--color--gray-20);
|
|
||||||
--color-text-neutral-loud: var(--color--gray-5);
|
|
||||||
--color-text-primary-medium: var(--val-color--primary-40);
|
|
||||||
--color-text-primary-loud: var(--val-color--primary-30);
|
|
||||||
--color--black: #000;
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
--color--red: #e33f1e;
|
|
||||||
--color--gold: #fdca40;
|
|
||||||
--color--green: #3fa21c;
|
|
||||||
--header-height-wide-when-fixed: calc(6 * var(--val-gap));
|
|
||||||
--mobile-nav-width: 31.25rem;
|
|
||||||
|
|
||||||
--val-menu--border-radius: 0.625rem;
|
|
||||||
*/
|
|
||||||
--val-border-radius: 0.375rem;
|
|
||||||
|
|
||||||
/* Menu component */
|
|
||||||
--val-menu--color-bg: var(--val-color--bg);
|
|
||||||
--val-menu--color-highlight: #e91e63;
|
|
||||||
--val-menu--color-border: rgba(0, 0, 0, 0.1);
|
|
||||||
--val-menu--color-shadow: rgba(0, 0, 0, 0.06);
|
|
||||||
--val-menu--line-padding: 0.625rem;
|
|
||||||
--val-menu--line-height: calc(1.875rem + 1px);
|
|
||||||
--val-menu--item-height: calc(var(--val-menu--line-padding) + var(--val-menu--line-height));
|
|
||||||
--val-menu--item-width-min: 14rem;
|
|
||||||
--val-menu--item-width-max: 20rem;
|
|
||||||
--val-menu--item-gap: 1rem;
|
|
||||||
--val-menu--trigger-width: 2.675rem;
|
|
||||||
--val-menu--side-width: 20rem;
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue