✨ (theme): Añade Breakpoint y ResponsiveStyles
- Nuevo `Breakpoint` (`Xs`/`Sm`/`Md`/`Lg`/`Xl`/`Xxl`) y `Theme::breakpoint_min_width()`, que traduce cada variante al ancho mínimo CSS del tema activo (mismo patrón que `Theme::intent_color()`). - `ResponsiveStyles` acumula declaraciones `property: value` agrupadas por punto de corte (opcional: `None` para reglas siempre activas, sin depender del tema) y por clase o clases. Aplica first-write-wins para clases utilitarias repetidas por muchos componentes. `render()` las agrupa en bloques `@media (min-width: ...)` mobile-first, sin saltos de línea.
This commit is contained in:
parent
b163b7726e
commit
f2a7507317
9 changed files with 760 additions and 11 deletions
|
|
@ -2,9 +2,9 @@ use crate::auth::CurrentUser;
|
|||
use crate::core::TypeInfo;
|
||||
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
||||
use crate::core::theme::all::DEFAULT_THEME;
|
||||
use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates};
|
||||
use crate::core::theme::{Breakpoint, ChildrenInRegions, CoreRegions, CoreTemplates};
|
||||
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
|
||||
use crate::html::{Assets, Favicon, JavaScript, Preload, ResponsiveStyles, StyleSheet};
|
||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||
use crate::locale::Lc;
|
||||
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
|
||||
|
|
@ -38,6 +38,11 @@ pub enum AssetsOp {
|
|||
AddJavaScript(JavaScript),
|
||||
/// Elimina un script por su ruta o identificador.
|
||||
RemoveJavaScript(&'static str),
|
||||
|
||||
/// Añade una declaración de estilo responsive (`property: value`) para las clases indicadas,
|
||||
/// dentro del punto de corte dado (`None` para una regla siempre activa). Ver
|
||||
/// [`ResponsiveStyles::add_style()`].
|
||||
AddResponsiveStyle(Option<Breakpoint>, &'static str, &'static str, &'static str),
|
||||
}
|
||||
|
||||
/// Errores de acceso a parámetros dinámicos del contexto.
|
||||
|
|
@ -217,6 +222,9 @@ pub trait Contextual: LangId {
|
|||
/// Devuelve los scripts JavaScript de los recursos del contexto.
|
||||
fn javascripts(&self) -> &Assets<JavaScript>;
|
||||
|
||||
/// Devuelve los estilos *responsive* acumulados en el contexto.
|
||||
fn responsive_styles(&self) -> &ResponsiveStyles;
|
||||
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del elemento `<body>`.
|
||||
fn body_props(&self) -> &Props;
|
||||
|
||||
|
|
@ -313,6 +321,7 @@ pub struct Context {
|
|||
preloads : Assets<Preload>, // Recursos para precarga.
|
||||
stylesheets : Assets<StyleSheet>, // Hojas de estilo CSS.
|
||||
javascripts : Assets<JavaScript>, // Scripts JavaScript.
|
||||
responsives : ResponsiveStyles, // Estilos *responsive*.
|
||||
body_props : Props, // Id, clases CSS y atributos del <body>.
|
||||
regions : ChildrenInRegions, // Regiones de componentes para renderizar.
|
||||
params : HashMap<&'static str, (Box<dyn Any + Send + Sync>, &'static str)>, // Parámetros.
|
||||
|
|
@ -345,6 +354,7 @@ impl Context {
|
|||
preloads : Assets::<Preload>::new(),
|
||||
stylesheets: Assets::<StyleSheet>::new(),
|
||||
javascripts: Assets::<JavaScript>::new(),
|
||||
responsives: ResponsiveStyles::new(),
|
||||
body_props : Props::default(),
|
||||
regions : ChildrenInRegions::default(),
|
||||
params : HashMap::default(),
|
||||
|
|
@ -401,6 +411,10 @@ impl Context {
|
|||
// Primero los recursos para precarga para iniciar las descargas inmediatamente.
|
||||
(preloads.render(self))
|
||||
(stylesheets.render(self))
|
||||
// Después los estilos *responsive*, para poder sobrescribir sus clases.
|
||||
@if !self.responsives.is_empty() {
|
||||
style { (self.responsives.render(self)) }
|
||||
}
|
||||
(javascripts.render(self))
|
||||
};
|
||||
|
||||
|
|
@ -604,6 +618,11 @@ impl Contextual for Context {
|
|||
AssetsOp::RemoveJavaScript(path) => {
|
||||
self.javascripts.remove(path);
|
||||
}
|
||||
// Estilos responsive.
|
||||
AssetsOp::AddResponsiveStyle(breakpoint, classes, property, value) => {
|
||||
self.responsives
|
||||
.add_style(breakpoint, classes, property, value);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
|
@ -664,6 +683,10 @@ impl Contextual for Context {
|
|||
&self.javascripts
|
||||
}
|
||||
|
||||
fn responsive_styles(&self) -> &ResponsiveStyles {
|
||||
&self.responsives
|
||||
}
|
||||
|
||||
fn body_props(&self) -> &Props {
|
||||
&self.body_props
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@
|
|||
//! 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 cinco pasos, cada uno necesario sólo si lo que ofrece PageTop
|
||||
//! por defecto no basta:
|
||||
//! 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`]
|
||||
|
|
@ -77,14 +77,22 @@
|
|||
//! 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.
|
||||
//! 4. **Traducir [`Intent`] a la paleta de colores propia del tema** sobrescribiendo
|
||||
//! 4. **Definir los anchos mínimos *mobile-first* para los puntos de corte** sobrescribiendo
|
||||
//! [`Theme::breakpoint_min_width()`]. Por defecto, [`Breakpoint`] resuelve el ancho mínimo de
|
||||
//! cada variante (`Sm`, `Md`, etc.) como una cadena CSS ya formateada (p. ej. `"768px"`) que
|
||||
//! cada tema puede adaptar. 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.
|
||||
//! 5. **Reexportar, extender o añadir componentes**. Un tema puede reexportar tal cual los
|
||||
//! 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()`](crate::html::PropsOp::set_extra) para consumirlos en el
|
||||
|
|
@ -137,6 +145,9 @@
|
|||
mod intent;
|
||||
pub use intent::Intent;
|
||||
|
||||
mod breakpoint;
|
||||
pub use breakpoint::Breakpoint;
|
||||
|
||||
mod layout;
|
||||
pub use layout::{CoreRegions, RegionName, RegionRef};
|
||||
pub use layout::{CoreTemplates, TemplateName, TemplateRef};
|
||||
|
|
|
|||
48
src/core/theme/breakpoint.rs
Normal file
48
src/core/theme/breakpoint.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::AutoDefault;
|
||||
use crate::core::component::{Context, Contextual};
|
||||
|
||||
// **< Breakpoint >*********************************************************************************
|
||||
|
||||
/// Puntos de corte *responsive*, *mobile-first* (aplican "a partir de" el ancho indicado).
|
||||
///
|
||||
/// No define ningún valor en píxeles por sí mismo; cada tema decide a qué ancho corresponde cada
|
||||
/// variante en su sistema de diseño (ver [`Theme::breakpoint_min_width()`]).
|
||||
///
|
||||
/// [`Theme::breakpoint_min_width()`]: crate::core::theme::Theme::breakpoint_min_width
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Breakpoint {
|
||||
/// Base *mobile-first*, equivale a "siempre".
|
||||
#[default]
|
||||
Xs,
|
||||
/// A partir del ancho donde un tema suele pasar de móvil a tableta.
|
||||
Sm,
|
||||
/// A partir del ancho donde un tema suele pasar a un escritorio pequeño.
|
||||
Md,
|
||||
/// A partir del ancho donde un tema suele pasar a un escritorio normal.
|
||||
Lg,
|
||||
/// A partir del ancho donde un tema suele considerar el escritorio ancho.
|
||||
Xl,
|
||||
/// A partir del ancho donde un tema suele considerar el escritorio muy ancho.
|
||||
Xxl,
|
||||
}
|
||||
|
||||
impl Breakpoint {
|
||||
// Todas las variantes, en orden mobile-first (de Xs a Xxl).
|
||||
pub(crate) const ALL: [Breakpoint; 6] = [
|
||||
Breakpoint::Xs,
|
||||
Breakpoint::Sm,
|
||||
Breakpoint::Md,
|
||||
Breakpoint::Lg,
|
||||
Breakpoint::Xl,
|
||||
Breakpoint::Xxl,
|
||||
];
|
||||
|
||||
/// Ancho mínimo resuelto a través del tema activo del contexto actual, como valor CSS ya
|
||||
/// formateado (p. ej. `"768px"`), o `""` si la variante se aplica siempre, sin ancho real.
|
||||
///
|
||||
/// Atajo de [`Theme::breakpoint_min_width()`](crate::core::theme::Theme::breakpoint_min_width)
|
||||
/// a través de [`Context::theme()`].
|
||||
pub fn min_width(&self, cx: &Context) -> &'static str {
|
||||
cx.theme().breakpoint_min_width(*self)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout};
|
|||
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
||||
use crate::core::component::{Context, Contextual};
|
||||
use crate::core::extension::Extension;
|
||||
use crate::core::theme::{CoreRegions, Intent};
|
||||
use crate::core::theme::{Breakpoint, CoreRegions, Intent};
|
||||
use crate::global;
|
||||
use crate::html::{Markup, html};
|
||||
use crate::locale::Lc;
|
||||
|
|
@ -65,19 +65,49 @@ pub trait Theme: Extension + Send + Sync {
|
|||
None
|
||||
}
|
||||
|
||||
/// Traduce un [`Breakpoint`] al punto de corte *responsive*, *mobile-first*, propio del tema.
|
||||
///
|
||||
/// `Breakpoint` no define ningún valor propio en píxeles. Será cada tema el que decida a qué
|
||||
/// ancho corresponde cada variante como valor CSS ya formateado (p. ej. `"768px"`), listo para
|
||||
/// aplicar en un `@media (min-width: ...)` sin ningún cálculo adicional. La cadena vacía (`""`)
|
||||
/// indica que la variante no representa ningún ancho mínimo y se aplica siempre; es el caso de
|
||||
/// `Xs`.
|
||||
///
|
||||
/// Normalmente, para resolver un ancho *responsive* no se llamará a este método directamente,
|
||||
/// sino que se usará [`Breakpoint::min_width()`] a través de [`Context::theme()`].
|
||||
///
|
||||
/// [`Breakpoint::min_width()`]: crate::core::theme::Breakpoint::min_width
|
||||
/// [`Context::theme()`]: crate::core::component::Context::theme
|
||||
#[rustfmt::skip]
|
||||
fn breakpoint_min_width(&self, bp: Breakpoint) -> &'static str {
|
||||
if let Some(parent) = self.parent() {
|
||||
return parent.breakpoint_min_width(bp);
|
||||
}
|
||||
match bp {
|
||||
Breakpoint::Xs => "",
|
||||
Breakpoint::Sm => "576px",
|
||||
Breakpoint::Md => "768px",
|
||||
Breakpoint::Lg => "992px",
|
||||
Breakpoint::Xl => "1200px",
|
||||
Breakpoint::Xxl => "1400px",
|
||||
}
|
||||
}
|
||||
|
||||
/// Traduce una [`Intent`] al nombre de color de la paleta propia del tema.
|
||||
///
|
||||
/// `Intent` no define ninguna cadena propia. Cada tema decide qué nombre le corresponde a cada
|
||||
/// variante en su paleta (p. ej. un tema basado en Bootstrap traduce `Severe` a `"danger"`).
|
||||
/// Los componentes que generan clases CSS a partir de una `Intent` (`Button`, `Badge`,
|
||||
/// `Dropdown`, etc.) no llaman a este método directamente en su `setup()`, usan mejor
|
||||
/// [`Intent::color()`](crate::core::theme::Intent::color) como la forma más sencilla de obtener
|
||||
/// este mismo valor a través de [`Context::theme()`](crate::core::component::Context::theme),
|
||||
/// para que la clase resultante ya nazca en la paleta del tema activo.
|
||||
/// [`Intent::color()`] como la forma más sencilla de obtener este mismo valor a través de
|
||||
/// [`Context::theme()`], para que la clase resultante ya nazca en la paleta del tema activo.
|
||||
///
|
||||
/// La implementación por defecto devuelve el vocabulario semántico propio de PageTop
|
||||
/// (`"primary"`, `"severe"`, etc.), que actúa como paleta base cuando ningún tema la
|
||||
/// sobrescribe.
|
||||
///
|
||||
/// [`Intent::color()`]: crate::core::theme::Intent::color
|
||||
/// [`Context::theme()`]: crate::core::component::Context::theme
|
||||
#[rustfmt::skip]
|
||||
fn intent_color(&self, intent: Intent) -> &'static str {
|
||||
if let Some(parent) = self.parent() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue