✨ (theme): Añade componentes Region y Template
- Incluye un componente base `Template` para gestionar la estructura del documento y sus regiones (`Region`). - Actualiza el *trait* `Contextual` para permitir la selección de la plantilla de renderizado. - Modifica `Page` y `Context`, y refactoriza el manejo de temas, para dar soporte al nuevo sistema de plantillas y eliminar la gestión obsoleta de regiones.
This commit is contained in:
parent
4a3244d0e4
commit
f0e5f50a7f
20 changed files with 506 additions and 475 deletions
|
|
@ -1,48 +1,46 @@
|
|||
//! Componentes nativos proporcionados por PageTop.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// **< FontSize >***********************************************************************************
|
||||
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum FontSize {
|
||||
ExtraLarge,
|
||||
XxLarge,
|
||||
XLarge,
|
||||
Large,
|
||||
Medium,
|
||||
#[default]
|
||||
Normal,
|
||||
Small,
|
||||
XSmall,
|
||||
XxSmall,
|
||||
ExtraSmall,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl FontSize {
|
||||
#[inline]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
FontSize::ExtraLarge => "fs__x3l",
|
||||
FontSize::XxLarge => "fs__x2l",
|
||||
FontSize::XLarge => "fs__xl",
|
||||
FontSize::Large => "fs__l",
|
||||
FontSize::Medium => "fs__m",
|
||||
FontSize::Normal => "",
|
||||
FontSize::Small => "fs__s",
|
||||
FontSize::XSmall => "fs__xs",
|
||||
FontSize::XxSmall => "fs__x2s",
|
||||
FontSize::ExtraSmall => "fs__x3s",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
|
|
@ -51,6 +49,3 @@ pub use intro::{Intro, IntroOpening};
|
|||
|
||||
mod poweredby;
|
||||
pub use poweredby::PoweredBy;
|
||||
|
||||
mod icon;
|
||||
pub use icon::{Icon, IconKind};
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub enum IconKind {
|
||||
#[default]
|
||||
None,
|
||||
Font(FontSize),
|
||||
Svg {
|
||||
shapes: Markup,
|
||||
viewbox: AttrValue,
|
||||
},
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Icon {
|
||||
classes : AttrClasses,
|
||||
icon_kind : IconKind,
|
||||
aria_label: AttrL10n,
|
||||
}
|
||||
|
||||
impl Component for Icon {
|
||||
fn new() -> Self {
|
||||
Icon::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
if !matches!(self.icon_kind(), IconKind::None) {
|
||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
||||
}
|
||||
if let IconKind::Font(font_size) = self.icon_kind() {
|
||||
self.alter_classes(ClassesOp::Add, font_size.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.icon_kind() {
|
||||
IconKind::None => PrepareMarkup::None,
|
||||
IconKind::Font(_) => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
PrepareMarkup::With(html! {
|
||||
i
|
||||
class=[self.classes().get()]
|
||||
role=[has_label.then_some("img")]
|
||||
aria-label=[aria_label]
|
||||
aria-hidden=[(!has_label).then_some("true")]
|
||||
{}
|
||||
})
|
||||
}
|
||||
IconKind::Svg { shapes, viewbox } => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
||||
PrepareMarkup::With(html! {
|
||||
svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox=(viewbox)
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
class=[self.classes().get()]
|
||||
role=[has_label.then_some("img")]
|
||||
aria-label=[aria_label]
|
||||
aria-hidden=[(!has_label).then_some("true")]
|
||||
{
|
||||
(shapes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn font() -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||
}
|
||||
|
||||
pub fn font_sized(font_size: FontSize) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(font_size))
|
||||
}
|
||||
|
||||
pub fn svg(shapes: Markup) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::new(viewbox),
|
||||
})
|
||||
}
|
||||
|
||||
// **< Icon BUILDER >***************************************************************************
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al icono.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self {
|
||||
self.icon_kind = icon_kind;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_aria_label(mut self, label: L10n) -> Self {
|
||||
self.aria_label.alter_value(label);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Icon GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
pub fn icon_kind(&self) -> &IconKind {
|
||||
&self.icon_kind
|
||||
}
|
||||
|
||||
pub fn aria_label(&self) -> &AttrL10n {
|
||||
&self.aria_label
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ use crate::prelude::*;
|
|||
// Enlace a la página oficial de PageTop.
|
||||
const LINK: &str = "<a href=\"https://pagetop.cillero.es\" rel=\"noopener noreferrer\">PageTop</a>";
|
||||
|
||||
/// Componente que renderiza la sección 'Powered by' (*Funciona con*) típica del pie de página.
|
||||
/// Componente que informa del 'Powered by' (*Funciona con*) típica del pie de página.
|
||||
///
|
||||
/// Por defecto, usando [`default()`](Self::default) sólo se muestra un reconocimiento a PageTop.
|
||||
/// Sin embargo, se puede usar [`new()`](Self::new) para crear una instancia con un texto de
|
||||
|
|
|
|||
150
src/base/component/region.rs
Normal file
150
src/base/component/region.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
84
src/base/component/template.rs
Normal file
84
src/base/component/template.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! Temas básicos soportados por PageTop.
|
||||
//! Tema básico soportados por PageTop.
|
||||
|
||||
mod basic;
|
||||
pub use basic::{Basic, BasicRegions};
|
||||
pub use basic::Basic;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
/// Es el tema básico que incluye PageTop por defecto.
|
||||
use crate::prelude::*;
|
||||
|
||||
/// El tema básico usa las mismas regiones predefinidas por [`DefaultRegions`].
|
||||
pub type BasicRegions = DefaultRegions;
|
||||
|
||||
/// Tema básico por defecto que extiende el funcionamiento predeterminado de [`Theme`].
|
||||
pub struct Basic;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue