(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:
Manuel Cillero 2025-11-22 09:11:16 +01:00
parent 4a3244d0e4
commit f0e5f50a7f
20 changed files with 506 additions and 475 deletions

View file

@ -63,10 +63,11 @@ theme = "Aliner"
```rust,no_run
use pagetop::prelude::*;
use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme("Aliner")
.with_theme(&Aliner)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))

View file

@ -64,10 +64,11 @@ theme = "Aliner"
```rust,no_run
use pagetop::prelude::*;
use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme("Aliner")
.with_theme(&Aliner)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
@ -82,15 +83,11 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
use pagetop::prelude::*;
/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`].
pub type AlinerRegions = DefaultRegions;
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
///
/// Tema mínimo ideal para **pruebas y demos** que renderiza el **esqueleto HTML** con las mismas
/// regiones básicas definidas por [`DefaultRegions`]. No pretende ser un tema para producción, está
/// pensado para:
/// Define un tema mínimo útil para:
///
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
/// - Realizar pruebas de renderizado rápido con salida estable y predecible.
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.

View file

@ -63,10 +63,11 @@ theme = "Bootsier"
```rust,no_run
use pagetop::prelude::*;
use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme("Bootsier")
.with_theme(&Bootsier)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))

View file

@ -64,10 +64,11 @@ theme = "Bootsier"
```rust,no_run
use pagetop::prelude::*;
use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme("Bootsier")
.with_theme(&Bootsier)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
@ -101,9 +102,6 @@ pub mod prelude {
pub use crate::theme::*;
}
/// El tema usa las mismas regiones predefinidas por [`DefaultRegions`].
pub type BootsierRegions = DefaultRegions;
/// Implementa el tema.
pub struct Bootsier;

View file

@ -0,0 +1,134 @@
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
}
}