♻️ 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),
|
||||
))
|
||||
.alter_child_in(
|
||||
Region::FOOTER,
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,8 @@
|
|||
//! 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;
|
||||
pub use html::Html;
|
||||
|
||||
mod region;
|
||||
pub use region::Region;
|
||||
|
||||
mod template;
|
||||
pub use template::Template;
|
||||
|
||||
mod 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),
|
||||
))
|
||||
.alter_child_in(
|
||||
Region::FOOTER,
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(Child::with(PoweredBy::new())),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::base::component::Template;
|
||||
use crate::core::component::ChildOp;
|
||||
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::html::{html, Markup};
|
||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
|
|
@ -68,7 +67,7 @@ pub enum ContextError {
|
|||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||
/// cx.with_langid(&LangMatch::resolve("es-ES"))
|
||||
/// .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::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||
/// .with_assets(ContextOp::AddJavaScript(JavaScript::defer("/js/app.js")))
|
||||
|
|
@ -92,7 +91,7 @@ pub trait Contextual: LangId {
|
|||
|
||||
/// Especifica la plantilla para renderizar el documento.
|
||||
#[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.
|
||||
#[builder_fn]
|
||||
|
|
@ -102,9 +101,9 @@ pub trait Contextual: LangId {
|
|||
#[builder_fn]
|
||||
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]
|
||||
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 >*********************************************************************
|
||||
|
||||
|
|
@ -114,8 +113,8 @@ pub trait Contextual: LangId {
|
|||
/// Devuelve el tema que se usará para renderizar el documento.
|
||||
fn theme(&self) -> ThemeRef;
|
||||
|
||||
/// Devuelve el nombre de la plantilla usada para renderizar el documento.
|
||||
fn template(&self) -> &str;
|
||||
/// Devuelve la plantilla configurada para renderizar el documento.
|
||||
fn template(&self) -> TemplateRef;
|
||||
|
||||
/// Recupera un parámetro como [`Option`].
|
||||
fn param<T: 'static>(&self, key: &'static str) -> Option<&T>;
|
||||
|
|
@ -208,7 +207,7 @@ pub struct Context {
|
|||
request : Option<HttpRequest>, // Solicitud HTTP de origen.
|
||||
langid : &'static LanguageIdentifier, // Identificador de idioma.
|
||||
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.
|
||||
stylesheets: Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts: Assets<JavaScript>, // Scripts JavaScript.
|
||||
|
|
@ -248,7 +247,7 @@ impl Context {
|
|||
request,
|
||||
langid,
|
||||
theme : *DEFAULT_THEME,
|
||||
template : Template::DEFAULT,
|
||||
template : DEFAULT_THEME.default_template(),
|
||||
favicon : None,
|
||||
stylesheets: Assets::<StyleSheet>::new(),
|
||||
javascripts: Assets::<JavaScript>::new(),
|
||||
|
|
@ -286,10 +285,10 @@ impl Context {
|
|||
markup
|
||||
}
|
||||
|
||||
/// Renderiza los componentes de la región `region_name`.
|
||||
pub fn render_region(&mut self, region_name: impl AsRef<str>) -> Markup {
|
||||
/// Renderiza los componentes de una región.
|
||||
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
|
||||
self.regions
|
||||
.children_for(self.theme, region_name)
|
||||
.children_for(self.theme, region_ref)
|
||||
.render(self)
|
||||
}
|
||||
|
||||
|
|
@ -417,8 +416,8 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_template(mut self, template_name: &'static str) -> Self {
|
||||
self.template = template_name;
|
||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||
self.template = template;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -474,8 +473,8 @@ impl Contextual for Context {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
||||
self.regions.alter_child_in(region_name, op);
|
||||
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||
self.regions.alter_child_in(region_ref, op);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -489,7 +488,7 @@ impl Contextual for Context {
|
|||
self.theme
|
||||
}
|
||||
|
||||
fn template(&self) -> &str {
|
||||
fn template(&self) -> TemplateRef {
|
||||
self.template
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,24 +4,23 @@ use crate::core::AnyInfo;
|
|||
use crate::locale::L10n;
|
||||
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.
|
||||
///
|
||||
/// Este *trait* es fácil de implementar, basta con declarar una estructura de tamaño cero para la
|
||||
/// extensión y sobreescribir los métodos que sea necesario.
|
||||
/// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la
|
||||
/// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// pub struct Blog;
|
||||
///
|
||||
/// impl Extension for Blog {
|
||||
/// fn name(&self) -> L10n { L10n::n("Blog") }
|
||||
/// fn description(&self) -> L10n { L10n::n("Blog system") }
|
||||
/// fn name(&self) -> L10n {
|
||||
/// L10n::n("Blog")
|
||||
/// }
|
||||
///
|
||||
/// fn description(&self) -> L10n {
|
||||
/// L10n::n("Blog system")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
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.
|
||||
///
|
||||
/// Por defecto devuelve un valor vacío (`L10n::default()`).
|
||||
fn description(&self) -> L10n {
|
||||
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
|
||||
/// la extensión no es un tema, este método devuelve `None` por defecto.
|
||||
/// Para ello, la implementación concreta debe ser una extensión que también implemente
|
||||
/// [`Theme`](crate::core::theme::Theme). Por defecto, asume que la extensión no es un tema y
|
||||
/// devuelve `None`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -61,17 +65,17 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
|
||||
/// Otras extensiones que deben habilitarse **antes** de esta.
|
||||
///
|
||||
/// PageTop las resolverá automáticamente respetando el orden durante el arranque de la
|
||||
/// aplicación.
|
||||
/// PageTop resolverá automáticamente estas dependencias respetando el orden durante el arranque
|
||||
/// de la aplicación.
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
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
|
||||
/// [peso](crate::Weight), permitiendo personalizar el comportamiento de la aplicación en puntos
|
||||
/// específicos.
|
||||
/// [peso](crate::Weight) (ver [`actions_boxed!`](crate::actions_boxed)), permitiendo
|
||||
/// personalizar el comportamiento de la aplicación en puntos específicos.
|
||||
fn actions(&self) -> Vec<ActionBox> {
|
||||
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
|
||||
/// estáticos, etc., usando [`ServiceConfig`](crate::service::web::ServiceConfig).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use pagetop::prelude::*;
|
||||
/// pub struct ExtensionSample;
|
||||
|
|
@ -98,11 +104,15 @@ pub trait Extension: AnyInfo + Send + Sync {
|
|||
#[allow(unused_variables)]
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
||||
|
||||
/// Permite crear extensiones para deshabilitar y desinstalar recursos de otras de versiones
|
||||
/// anteriores de la aplicación.
|
||||
/// Permite declarar extensiones destinadas a deshabilitar o desinstalar recursos de otras
|
||||
/// 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> {
|
||||
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.
|
||||
//!
|
||||
//! En PageTop un tema es la *piel* de la aplicación. Es responsable último de los estilos,
|
||||
//! tipografías, espaciados y cualquier otro detalle visual o interactivo (animaciones, scripts de
|
||||
//! interfaz, etc.).
|
||||
//! Los temas son extensiones que implementan [`Extension`](crate::core::extension::Extension) y
|
||||
//! también [`Theme`], de modo que [`Extension::theme()`](crate::core::extension::Extension::theme)
|
||||
//! 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
|
||||
//! componentes ni la estructura del documento, que queda definida por la plantilla
|
||||
//! ([`Template`](crate::base::component::Template)) utilizada por cada página.
|
||||
//! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos
|
||||
//! interactivos. Para ello utiliza plantillas ([`Template`]) que describen cómo maquetar el cuerpo
|
||||
//! 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
|
||||
//! lo que se instancian, declaran dependencias y se inician igual que cualquier otra extensión.
|
||||
//! También deben implementar [`Theme`] y sobrescribir el método
|
||||
//! [`Extension::theme()`](crate::core::extension::Extension::theme) para que PageTop pueda
|
||||
//! registrarlos como temas.
|
||||
//! Una página ([`Page`](crate::response::page::Page)) representa un documento HTML completo.
|
||||
//! Implementa [`Contextual`](crate::core::component::Contextual) para gestionar su propio
|
||||
//! [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes
|
||||
//! asociados a cada región.
|
||||
//!
|
||||
//! 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;
|
||||
pub use definition::{Theme, ThemeRef};
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
use crate::base::component::Template;
|
||||
use crate::core::component::{ComponentRender, ContextOp, Contextual};
|
||||
use crate::core::component::Contextual;
|
||||
use crate::core::extension::Extension;
|
||||
use crate::global;
|
||||
use crate::html::{html, Markup, StyleSheet};
|
||||
use crate::html::{html, Markup};
|
||||
use crate::locale::L10n;
|
||||
use crate::prelude::{DefaultTemplate, TemplateRef};
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// ([`Template`]) y qué contenido mostrar en las páginas de error.
|
||||
/// las páginas: cómo se renderiza el `<head>`, cómo se presenta el `<body>` usando plantillas
|
||||
/// ([`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
|
||||
/// sobrescribirse selectivamente para crear nuevos temas con comportamientos distintos a los
|
||||
/// predeterminados.
|
||||
///
|
||||
/// 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
|
||||
/// # use pagetop::prelude::*;
|
||||
|
|
@ -47,32 +43,55 @@ pub type ThemeRef = &'static dyn Theme;
|
|||
/// impl Theme for MyTheme {}
|
||||
/// ```
|
||||
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.
|
||||
///
|
||||
/// Se invoca antes de que se procese la plantilla ([`Template`]) asociada a la página
|
||||
/// ([`Page::template()`](crate::response::page::Page::template)). Es un buen lugar para
|
||||
/// inicializar o ajustar recursos en función del contexto de la página, por ejemplo:
|
||||
/// Es un buen lugar para 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.
|
||||
/// - Registrar *assets* condicionales en el contexto.
|
||||
///
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
fn before_render_page_body(&self, page: &mut Page) {}
|
||||
|
||||
/// Renderiza el contenido del `<body>` de la página.
|
||||
///
|
||||
/// Por defecto, delega en la plantilla ([`Template`]) asociada a la página
|
||||
/// ([`Page::template()`](crate::response::page::Page::template)). La plantilla se encarga de
|
||||
/// procesar las regiones y renderizar los componentes registrados en el contexto.
|
||||
/// La implementación predeterminada delega en la plantilla asociada a la página, obtenida desde
|
||||
/// su [`Context`](crate::core::component::Context), y llama a
|
||||
/// [`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:
|
||||
///
|
||||
/// - 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.
|
||||
#[inline]
|
||||
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.
|
||||
|
|
@ -83,6 +102,8 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// - Realizar *tracing* o recopilar métricas.
|
||||
/// - Aplicar ajustes finales al estado de la página antes de producir el `<head>` o la
|
||||
/// respuesta final.
|
||||
///
|
||||
/// La implementación por defecto no realiza ninguna acción.
|
||||
#[allow(unused_variables)]
|
||||
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.
|
||||
/// - Los metadatos (`name`/`content`) y propiedades (`property`/`content`) declarados en la
|
||||
/// página.
|
||||
/// - Los *assets* registrados en el contexto de la página. Si el parámetro
|
||||
/// `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 *assets* registrados en el contexto de la página.
|
||||
///
|
||||
/// Los temas pueden sobrescribir este método para añadir etiquetas adicionales (por ejemplo,
|
||||
/// *favicons* personalizados, manifest, etiquetas de analítica, etc.).
|
||||
#[inline]
|
||||
fn render_page_head(&self, page: &mut Page) -> Markup {
|
||||
if page.param_or("include_basic_assets", false) {
|
||||
let pkg_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/root.css")
|
||||
.with_version(pkg_version)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(pkg_version)
|
||||
.with_weight(-99),
|
||||
));
|
||||
}
|
||||
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||
html! {
|
||||
meta charset="utf-8";
|
||||
|
|
@ -173,3 +171,6 @@ pub trait Theme: Extension + Send + Sync {
|
|||
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::theme::ThemeRef;
|
||||
use crate::core::theme::{DefaultRegion, RegionRef, ThemeRef};
|
||||
use crate::{builder_fn, AutoDefault, UniqueId};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
|
@ -21,24 +20,23 @@ static COMMON_REGIONS: LazyLock<RwLock<ChildrenInRegions>> =
|
|||
pub(crate) struct ChildrenInRegions(HashMap<String, Children>);
|
||||
|
||||
impl ChildrenInRegions {
|
||||
pub fn with(region_name: impl AsRef<str>, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_name, ChildOp::Add(child))
|
||||
pub fn with(region_ref: RegionRef, child: Child) -> Self {
|
||||
Self::default().with_child_in(region_ref, ChildOp::Add(child))
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
||||
let name = region_name.as_ref();
|
||||
if let Some(region) = self.0.get_mut(name) {
|
||||
pub fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||
if let Some(region) = self.0.get_mut(region_ref.name()) {
|
||||
region.alter_child(op);
|
||||
} else {
|
||||
self.0
|
||||
.insert(name.to_owned(), Children::new().with_child(op));
|
||||
.insert(region_ref.name().to_owned(), Children::new().with_child(op));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_name: impl AsRef<str>) -> Children {
|
||||
let name = region_name.as_ref();
|
||||
pub fn children_for(&self, theme_ref: ThemeRef, region_ref: RegionRef) -> Children {
|
||||
let name = region_ref.name();
|
||||
let common = COMMON_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
|
||||
/// disponibles durante toda la ejecución.
|
||||
///
|
||||
/// Estos componentes se renderizarán automáticamente al procesar los documentos HTML que incluyen
|
||||
/// estas regiones, como las páginas de contenido ([`Page`](crate::response::page::Page)).
|
||||
/// Cada variante indica la región en la que se añade el componente usando [`Self::add()`]. Los
|
||||
/// 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
|
||||
/// ([`Page`](crate::response::page::Page)).
|
||||
pub enum InRegion {
|
||||
/// Región de contenido por defecto.
|
||||
Default,
|
||||
/// Región identificada por el nombre proporcionado.
|
||||
Named(&'static str),
|
||||
/// Región identificada por su nombre para un tema concreto.
|
||||
OfTheme(&'static str, ThemeRef),
|
||||
/// Región principal de **contenido** por defecto.
|
||||
///
|
||||
/// Añade el componente a la región lógica de contenido principal de la aplicación. Por
|
||||
/// convención, esta región corresponde a [`DefaultRegion::Content`], cuyo nombre es
|
||||
/// `"content"`. Cualquier tema que renderice esa misma región de contenido, ya sea usando
|
||||
/// 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 {
|
||||
|
|
@ -73,28 +87,33 @@ impl InRegion {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// // Banner global, en la región por defecto de cualquier página.
|
||||
/// InRegion::Default.add(Child::with(Html::with(|_|
|
||||
/// html! { ("🎉 ¡Bienvenido!") }
|
||||
/// )));
|
||||
/// // Banner global en la región por defecto.
|
||||
/// InRegion::Content.add(Child::with(Html::with(|_| {
|
||||
/// html! { "🎉 ¡Bienvenido!" }
|
||||
/// })));
|
||||
///
|
||||
/// // Texto en la región "sidebar".
|
||||
/// InRegion::Named("sidebar").add(Child::with(Html::with(|_|
|
||||
/// html! { ("Publicidad") }
|
||||
/// )));
|
||||
/// // Texto en la cabecera.
|
||||
/// InRegion::Global(&DefaultRegion::Header).add(Child::with(Html::with(|_| {
|
||||
/// 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 {
|
||||
match self {
|
||||
InRegion::Default => Self::add_to_common(Region::DEFAULT, child),
|
||||
InRegion::Named(region_name) => Self::add_to_common(region_name, child),
|
||||
InRegion::OfTheme(region_name, theme_ref) => {
|
||||
InRegion::Content => Self::add_to_common(&DefaultRegion::Content, child),
|
||||
InRegion::Global(region_ref) => Self::add_to_common(*region_ref, child),
|
||||
InRegion::ForTheme(theme_ref, region_ref) => {
|
||||
let mut regions = THEME_REGIONS.write();
|
||||
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 {
|
||||
regions.insert(
|
||||
theme_ref.type_id(),
|
||||
ChildrenInRegions::with(region_name, child),
|
||||
ChildrenInRegions::with(*region_ref, child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +122,9 @@ impl InRegion {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
fn add_to_common(region_name: &str, child: Child) {
|
||||
fn add_to_common(region_ref: RegionRef, child: Child) {
|
||||
COMMON_REGIONS
|
||||
.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;
|
||||
pub use error::ErrorPage;
|
||||
|
||||
pub use actix_web::Result as ResultPage;
|
||||
|
||||
use crate::base::action;
|
||||
use crate::base::component::Region;
|
||||
use crate::core::component::{Child, ChildOp, Component, ComponentRender};
|
||||
use crate::core::component::{Child, ChildOp, Component};
|
||||
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::{Assets, Favicon, JavaScript, StyleSheet};
|
||||
use crate::html::{AttrClasses, ClassesOp};
|
||||
|
|
@ -16,6 +30,57 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
|||
use crate::service::HttpRequest;
|
||||
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.
|
||||
///
|
||||
/// 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>`.
|
||||
#[builder_fn]
|
||||
pub fn with_property(mut self, property: &'static str, content: &'static str) -> Self {
|
||||
self.metadata.push((property, content));
|
||||
self.properties.push((property, content));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -97,15 +162,17 @@ impl Page {
|
|||
|
||||
/// Añade un componente hijo a la región de contenido por defecto.
|
||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
||||
self.context
|
||||
.alter_child_in(Region::DEFAULT, ChildOp::Add(Child::with(component)));
|
||||
self.context.alter_child_in(
|
||||
&DefaultRegion::Content,
|
||||
ChildOp::Add(Child::with(component)),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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
|
||||
.alter_child_in(region_name, ChildOp::Add(Child::with(component)));
|
||||
.alter_child_in(region_ref, ChildOp::Add(Child::with(component)));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -154,8 +221,30 @@ impl Page {
|
|||
|
||||
/// Renderiza la página completa en formato HTML.
|
||||
///
|
||||
/// Ejecuta las acciones correspondientes antes y después de renderizar el `<body>`,
|
||||
/// así como del `<head>`, e inserta los atributos `lang` y `dir` en la etiqueta `<html>`.
|
||||
/// El proceso de renderizado de la página sigue esta secuencia:
|
||||
///
|
||||
/// 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> {
|
||||
// Acciones específicas del tema antes de renderizar el <body>.
|
||||
self.context.theme().before_render_page_body(self);
|
||||
|
|
@ -165,9 +254,9 @@ impl Page {
|
|||
|
||||
// Renderiza el <body>.
|
||||
let body = html! {
|
||||
(Region::named(Region::PAGETOP).render(&mut self.context))
|
||||
(ReservedRegion::PageTop.render(&mut self.context))
|
||||
(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>.
|
||||
|
|
@ -228,8 +317,8 @@ impl Contextual for Page {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_template(mut self, template_name: &'static str) -> Self {
|
||||
self.context.alter_template(template_name);
|
||||
fn with_template(mut self, template: TemplateRef) -> Self {
|
||||
self.context.alter_template(template);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -246,8 +335,8 @@ impl Contextual for Page {
|
|||
}
|
||||
|
||||
#[builder_fn]
|
||||
fn with_child_in(mut self, region_name: impl AsRef<str>, op: ChildOp) -> Self {
|
||||
self.context.alter_child_in(region_name, op);
|
||||
fn with_child_in(mut self, region_ref: RegionRef, op: ChildOp) -> Self {
|
||||
self.context.alter_child_in(region_ref, op);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +350,7 @@ impl Contextual for Page {
|
|||
self.context.theme()
|
||||
}
|
||||
|
||||
fn template(&self) -> &str {
|
||||
fn template(&self) -> TemplateRef {
|
||||
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::theme::DefaultTemplate;
|
||||
use crate::locale::L10n;
|
||||
use crate::response::ResponseError;
|
||||
use crate::service::http::{header::ContentType, StatusCode};
|
||||
|
|
@ -7,8 +8,21 @@ use crate::service::{HttpRequest, HttpResponse};
|
|||
|
||||
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)]
|
||||
pub enum ErrorPage {
|
||||
NotModified(HttpRequest),
|
||||
|
|
@ -20,7 +34,7 @@ pub enum ErrorPage {
|
|||
Timeout(HttpRequest),
|
||||
}
|
||||
|
||||
impl Display for ErrorPage {
|
||||
impl fmt::Display for ErrorPage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
// Error 304.
|
||||
|
|
@ -33,7 +47,7 @@ impl Display for ErrorPage {
|
|||
let error403 = error_page.theme().error403(&mut error_page);
|
||||
if let Ok(page) = error_page
|
||||
.with_title(L10n::n("Error FORBIDDEN"))
|
||||
.with_template(Template::ERROR)
|
||||
.with_template(&DefaultTemplate::Error)
|
||||
.add_child(Html::with(move |_| error403.clone()))
|
||||
.render()
|
||||
{
|
||||
|
|
@ -48,7 +62,7 @@ impl Display for ErrorPage {
|
|||
let error404 = error_page.theme().error404(&mut error_page);
|
||||
if let Ok(page) = error_page
|
||||
.with_title(L10n::n("Error RESOURCE NOT FOUND"))
|
||||
.with_template(Template::ERROR)
|
||||
.with_template(&DefaultTemplate::Error)
|
||||
.add_child(Html::with(move |_| error404.clone()))
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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