//! API para añadir y gestionar nuevos temas. //! //! Un tema es la *piel* de la aplicación: define estilos, tipografías, espaciados o comportamientos //! interactivos. Usa plantillas ([`Template`]) para maquetar los contenidos en base a regiones //! ([`Region`]). Cada región es un contenedor lógico identificado por un nombre para agrupar y //! renderizar componentes. //! //! Una página ([`Page`]) es un documento HTML completo. Implementa [`Contextual`] para gestionar su //! propio [`Context`], donde mantiene el tema activo, la plantilla seleccionada y los componentes //! asociados a cada región a renderizar. //! //! # Temas hijo, herencia y componentes //! //! PageTop permite crear **temas hijo** que refinan el comportamiento de su tema padre, //! identificado por [`Theme::parent()`]. Un tema hijo hereda automáticamente todos los métodos del //! padre y puede sobrescribirlos selectivamente. Esta herencia determina qué implementación de sus //! métodos se usa cuando el tema hijo no los sobrescribe, ya sea el renderizado del `` o del //! ``, la definición de los recursos necesarios, la traducción de puntos de corte con //! [`Theme::breakpoint_entry()`] y colores según intención vía [`Theme::intent_color()`], la //! captura de componentes para alterar su comportamiento usando [`Theme::setup_component()`] y //! [`Theme::render_component()`], las páginas de error, etc. //! //! Un tema hijo puede ser a su vez padre de otro, basta declararlo cada vez en [`Theme::parent()`]. //! Como `parent()` se resuelve en tiempo de ejecución, PageTop no puede descartar en compilación //! referencias circulares (un tema acaba siendo padre de sí mismo, directa o transitivamente). Ese //! ciclo causaría un bucle infinito recorriendo la cadena de temas en [`Theme::setup_component()`] //! y [`Theme::render_component()`], o un desbordamiento de pila en los métodos predefinidos de //! `Theme` que delegan recursivamente en el padre. Para evitarlo, PageTop recorre la cadena de cada //! tema al registrarlo y **aborta el arranque de la aplicación** si detecta una referencia //! circular. //! //! Sin embargo, no dice nada sobre los componentes. Aunque un tema puede exportar su propio //! catálogo de componentes, realmente no pertenecen como tal a ningún tema ni dependen de esa //! cadena de herencia. Una extensión puede existir únicamente para aportar un componente genérico //! (por ejemplo, un editor de texto enriquecido) pensado para usarse en cualquier aplicación, con //! independencia del tema activo. Que un tema decida capturar ese componente en //! [`Theme::setup_component()`] o [`Theme::render_component()`] para adaptarlo es una decisión //! propia del tema, no una relación de parentesco. Cualquier tema de la cadena de herencia puede //! interceptar cualquier componente, venga de la extensión que venga, sin que exista ningún vínculo //! de diseño previo entre ambos. //! //! Lo que sí es responsabilidad del tema activo es garantizar que el componente disponga de los //! recursos que necesita para verse y comportarse correctamente: sus propios estilos y JavaScript, //! si los aporta, o los que ofrezca el tema. El componente genera su marcado igual aunque esos //! recursos falten, pero el resultado seguramente no lucirá ni funcionará como se espera. //! //! # Cómo crear un tema nuevo //! //! Un tema mínimo es una extensión que implementa [`Extension`] y también [`Theme`] para que //! [`Extension::theme()`] devuelva `Some(&Self)`. Basta con un `impl Theme for MyTheme {}` vacío, //! ya que todos los métodos de [`Theme`] tienen implementación por defecto. //! //! Un tema puede personalizarse en seis pasos, cada uno necesario sólo si lo que ofrece PageTop //! por defecto no basta o no aplica: //! //! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`, //! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`] //! (`PageTop`, `PageBottom`) como regiones reservadas que se renderizan al margen de cualquier //! plantilla. Un tema puede definir su propio *enum* que implemente [`RegionName`] para //! **añadir** nuevas regiones que PageTop no ofrece (como barras laterales, regiones específicas //! para menús, sliders, cabeceras hero, etc.). No es necesario redefinir las de [`CoreRegions`] //! ni las de [`ReservedRegions`], que ya existen y se asume que cualquier tema respeta. //! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplates`], con las plantillas //! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son //! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada //! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para //! **añadir** plantillas que PageTop no ofrece, y no para redefinir `Standard`/`Admin`. //! 3. **Cambiar cómo se renderiza o se ajusta** una región, una plantilla o un componente ya //! existente. Para sobrescribir su renderizado se captura el componente (por ejemplo, [`Region`] //! o [`Template`], o el componente que sea) usando [`Theme::render_component()`]. En el caso de //! regiones y plantillas, para distinguir *qué* región o plantilla concreta envuelve el //! componente, sin comparar cadenas, basta con encadenar el *getter* correspondiente //! ([`Region::region()`](crate::base::component::layout::Region::region) o //! [`Template::template()`](crate::base::component::layout::Template::template)) con //! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por //! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente //! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias //! variantes de plantilla. Para ajustarlo sin rehacer su marcado (añadir una clase, un //! atributo, etc.), se usa [`Theme::setup_component()`] en su lugar. //! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo //! [`Theme::breakpoint_entry()`]. [`Breakpoint`] no define ningún ancho propio; la //! implementación por defecto de este método resuelve el ancho mínimo de cada variante (`Sm`, //! `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`), que cada tema puede //! sobrescribir. Cuando se genera CSS *responsive* a partir de un [`Breakpoint`], se consulta el //! punto de corte a través de [`Breakpoint::min_width()`], listo para interpolar en un //! `@media (min-width: ...)` sin ningún cálculo adicional. Un tema sin diseño *responsive* puede //! traducir todas las variantes a `""` porque al ser *mobile-first*, un punto de corte sin ancho //! real se aplicará siempre. //! 5. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo //! [`Theme::intent_color()`]. Por defecto, este método devuelve el vocabulario semántico de //! [`Intent`] (`"primary"`, `"severe"`, etc.); un tema con su propio catálogo de colores (por //! ejemplo, uno basado en Bootstrap) debe traducir cada variante al nombre que le corresponda en //! su paleta. Los componentes que generan clases CSS a partir de una [`Intent`] (`Button`, //! `Badge`, `Dropdown`, etc.) consultan este método a través de [`Intent::color()`], así que la //! clase resultante ya nace en la paleta del tema activo. //! 6. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los //! componentes propios de PageTop que no requieran adaptación, extenderlos con un trait propio //! para añadir métodos exclusivos (guardando su estado en valores extra con //! [`PropsOp::set_extra()`] para consumirlos en el `setup()` vía [`Theme::setup_component()`] o //! en el `render()` vía [`Theme::render_component()`]), o aportar componentes propios. //! `pagetop-bootsier` combina las tres estrategias: reexporta `Form`/`Fieldset` sin cambios, //! extiende `Button`/`Badge`/`Dropdown`/`Nav`/`Navbar` con sus propios traits (`ButtonBootsier`, //! `BadgeBootsier`, etc.), y añade componentes propios como `Offcanvas`. //! //! Para forzar una plantilla completamente distinta en una página concreta, se puede llamar //! manualmente a [`with_template()`]. //! //! Las páginas de error (403, 404, y otros errores fatales) no tienen una plantilla propia: se //! renderizan con la plantilla ya activa en la página, para que el usuario no pierda el contexto de //! navegación del sitio. Los temas pueden personalizar su contenido sobrescribiendo //! [`Theme::error_403()`], [`Theme::error_404()`] o [`Theme::error_fatal()`], sin necesidad de una //! plantilla distinta. //! //! El resto del comportamiento de un tema (por ejemplo, el renderizado del ``) se sobrescribe //! de forma independiente de estos pasos y no es necesario para tener un tema funcional. //! //! # Componentes que se procesan en todas las páginas //! //! Los componentes añadidos a una página con [`with_child_in()`] sólo existen para esa petición //! concreta: hay que volver a añadirlos cada vez que se construya la página. [`InRegion`] resuelve //! el caso contrario: un componente que se debe procesar en todas las páginas, o en todas las de un //! tema concreto, sin tener que registrarlo en el código de cada página. //! //! `InRegion` registra el componente una sola vez, normalmente al arrancar la aplicación o al //! inicializar una extensión, y a partir de ahí se procesa automáticamente en todas las páginas que //! correspondan: //! //! ```rust,no_run //! # use pagetop::prelude::*; //! InRegion::Global(&CoreRegions::Footer).add(PoweredBy::new()); //! ``` //! //! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del //! renderizado, de modo que su `setup()` siempre parte de un estado inicial limpio y no acumula //! mutaciones entre peticiones. //! //! Como cualquier otro componente, antes de renderizarse pasa por [`is_renderable()`], el primer //! paso del [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite //! registrarlo una sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta //! de la petición o si el usuario actual está autenticado. //! //! [`PropsOp::set_extra()`]: crate::html::PropsOp::set_extra //! [`ReservedRegions`]: crate::response::ReservedRegions //! [`Page`]: crate::response::Page //! [`Extension`]: crate::core::extension::Extension //! [`Extension::theme()`]: crate::core::extension::Extension::theme //! [`Context`]: crate::core::component::Context //! [`Contextual`]: crate::core::component::Contextual //! [`with_template()`]: crate::core::component::Contextual::with_template //! [`with_child_in()`]: crate::core::component::Contextual::with_child_in //! [`is_renderable()`]: crate::core::component::Component::is_renderable //! [`Template`]: crate::base::component::layout::Template //! [`Region`]: crate::base::component::layout::Region mod intent; pub use intent::Intent; mod breakpoint; pub use breakpoint::{Breakpoint, BreakpointEntry, Responsive}; mod layout; pub use layout::{CoreRegions, RegionName, RegionRef}; pub use layout::{CoreTemplates, TemplateName, TemplateRef}; mod definition; pub use definition::{Theme, ThemeRef}; mod regions; pub(crate) use regions::ChildrenInRegions; pub use regions::InRegion; pub(crate) mod all;