✨ (pagetop): Añade PageTopSvg::Tile y ajusta Image

This commit is contained in:
Manuel Cillero 2026-09-21 13:51:18 +02:00
parent 16eec316c4
commit f8f023b513
5 changed files with 263 additions and 18 deletions

View file

@ -44,18 +44,11 @@ impl Component for Image {
fn setup(&mut self, _cx: &mut Context) {
self.alter_prop(PropsOp::prepend_classes(match self.source() {
image::Source::Logo(_) => "image image-fluid",
image::Source::Responsive(_) => "image image-fluid",
image::Source::Logo(_) | image::Source::Responsive(_) => "image image-fluid",
image::Source::Thumbnail(_) => "image image-thumbnail",
image::Source::Plain(_) => "image",
}));
// Se asigna un tamaño predefinido para el logotipo que `with_size()` puede sobrescribir.
if matches!(self.source(), image::Source::Logo(_)) {
self.alter_prop(PropsOp::add_style("width", "1.25em"));
self.alter_prop(PropsOp::add_style("height", "1.25em"));
}
// El tamaño se aplica como declaraciones `style` individuales sobre `Props`.
match *self.size() {
image::Size::Auto => {}
@ -74,6 +67,15 @@ impl Component for Image {
self.alter_prop(PropsOp::add_style("height", v.to_string()));
}
}
// El logotipo es cuadrado. Sin `width`/`height` toma un tamaño predefinido.
if matches!(self.source(), image::Source::Logo(_))
&& self.props().get_style("width").is_none()
&& self.props().get_style("height").is_none()
{
self.alter_prop(PropsOp::add_style("width", "1.25rem"));
self.alter_prop(PropsOp::add_style("height", "1.25rem"));
}
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
@ -81,9 +83,9 @@ impl Component for Image {
image::Source::Logo(svg) => {
return Ok(svg.markup_with(cx, self.props(), self.alternative().clone()));
}
image::Source::Responsive(source) => Some(source),
image::Source::Thumbnail(source) => Some(source),
image::Source::Plain(source) => Some(source),
image::Source::Responsive(source)
| image::Source::Thumbnail(source)
| image::Source::Plain(source) => Some(source),
};
Ok(html! {
img
@ -121,7 +123,7 @@ impl Image {
self
}
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
/// Establece el origen de la imagen.
pub fn with_source(mut self, source: image::Source) -> Self {
self.source = source;
self
@ -146,8 +148,8 @@ impl From<image::Source> for Image {
impl From<image::Source> for Option<Image> {
/// Permite pasar un [`image::Source`] directamente donde se espera `impl Into<Option<Image>>`
/// (p. ej. [`Brand::with_image()`](super::super::Brand::with_image)), sin construir la
/// [`Image`] a mano.
/// (p. ej. [`Brand::with_image()`](crate::base::component::Brand::with_image)), sin construir
/// la [`Image`] a mano.
fn from(source: image::Source) -> Self {
Some(Image::with(source))
}

View file

@ -7,7 +7,9 @@ use crate::prelude::*;
pub enum Size {
/// Ajuste automático por defecto.
///
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica. El logotipo
/// ([`Source::Logo`]) toma un tamaño predefinido de `1.25rem` de ancho y alto, salvo que se
/// haya fijado alguna de sus dimensiones.
#[default]
Auto,
/// Establece explícitamente el **ancho y alto** de la imagen.
@ -40,6 +42,10 @@ pub enum Size {
#[derive(AutoDefault, Clone, Debug, PartialEq)]
pub enum Source {
/// Imagen con el logotipo de PageTop.
///
/// Se renderiza como un `<svg>` en línea, siempre cuadrado. Sin dimensiones explícitas
/// ([`Size::Auto`]) mide `1.25rem` de ancho y alto; si se fija sólo una, la otra se ajusta
/// proporcionalmente.
#[default]
Logo(PageTopSvg),
/// Imagen que se adapta automáticamente a su contenedor.

View file

@ -3,6 +3,11 @@ use crate::core::component::Context;
use crate::html::{Markup, Props, html};
use crate::locale::Lc;
// Color oscuro del logotipo para `LineDark` y el fondo predeterminado de `Tile`.
const DARK: (u8, u8, u8) = (10, 11, 9);
// Color claro del logotipo para `LineLight` y línea blanca de `Tile`.
const LIGHT: (u8, u8, u8) = (255, 255, 255);
/// Representación SVG del **logotipo de PageTop** para incrustar en HTML.
///
/// # Ejemplo
@ -23,10 +28,15 @@ use crate::locale::Lc;
/// div class="line_red" {
/// (PageTopSvg::LineRGB(255, 0, 0).markup())
/// }
/// div class="tile" {
/// (PageTopSvg::Tile.markup())
/// }
/// div class="tile_blue" {
/// (PageTopSvg::TileRGB(0, 82, 204).markup())
/// }
/// }
/// };
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum PageTopSvg {
/// Versión por defecto con el logotipo a color.
@ -38,6 +48,12 @@ pub enum PageTopSvg {
LineLight,
/// Versión monocroma configurable por RGB.
LineRGB(u8, u8, u8),
/// Versión icono de aplicación. Muestra un recuadro de fondo oscuro y esquinas redondeadas
/// (salvo la inferior derecha), con el logotipo ampliado y recortado en línea clara.
Tile,
/// Igual que [`Tile`](Self::Tile), aunque el color del recuadro de fondo se configura por RGB.
/// El logotipo se muestra con línea clara u oscura según su contraste con el fondo.
TileRGB(u8, u8, u8),
}
impl PageTopSvg {
@ -94,9 +110,11 @@ impl PageTopSvg {
fn path_fills(&self) -> Markup {
match self {
Self::Color => self.logo_color(),
Self::LineDark => self.logo_line(10, 11, 9),
Self::LineLight => self.logo_line(255, 255, 255),
Self::LineDark => self.logo_line(DARK.0, DARK.1, DARK.2),
Self::LineLight => self.logo_line(LIGHT.0, LIGHT.1, LIGHT.2),
Self::LineRGB(r, g, b) => self.logo_line(*r, *g, *b),
Self::Tile => self.logo_tile(DARK.0, DARK.1, DARK.2),
Self::TileRGB(r, g, b) => self.logo_tile(*r, *g, *b),
}
}
@ -125,4 +143,37 @@ impl PageTopSvg {
path fill=(logo_rgb) d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
}
}
fn logo_tile(&self, r: u8, g: u8, b: u8) -> Markup {
// Luminancia relativa (WCAG 2.x), a partir de los canales sRGB.
let luminance = |(r, g, b): (u8, u8, u8)| {
let linear = |c: u8| {
let c = f32::from(c) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
};
0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
};
// Elige la línea, oscura o clara, con más contraste sobre el fondo. El contraste entre dos
// luminancias es `(L_mayor + 0.05) / (L_menor + 0.05)`.
let bg = luminance((r, g, b)) + 0.05;
let (lr, lg, lb) = if bg / (luminance(DARK) + 0.05) > (luminance(LIGHT) + 0.05) / bg {
DARK
} else {
LIGHT
};
// El logotipo se amplía y desplaza para recortar todo salvo la parte superior izquierda.
let fill = format!("rgb({r},{g},{b})");
html! {
rect width="1614" height="1614" rx="200" fill=(fill) {}
rect x="1414" y="1414" width="200" height="200" fill=(fill) {}
g transform="translate(161.4 161.4) scale(1.375)" {
(self.logo_line(lr, lg, lb))
}
}
}
}