♻️ (bootsier): Mueve Image al core de PageTop

`Image`, `Size` y `Source` pasan a `pagetop::base::component` como
componente genérico reutilizable por cualquier tema. Bootsier
sustituye las clases semánticas por las de Bootstrap (`img-fluid`,
`img-thumbnail`) interceptando `setup()`.
This commit is contained in:
Manuel Cillero 2026-08-17 00:55:35 +02:00
parent 745d492d5b
commit c3ff8a6ff8
6 changed files with 97 additions and 88 deletions

View file

@ -145,6 +145,7 @@ impl Theme for Bootsier {
setup_component!(component, {
Button => |c| theme::bs::button::setup(c),
Container => |c| theme::bs::container::setup(c),
Image => |c| theme::bs::image::setup(c),
form::input::Field => |c| theme::bs::form::input::setup(c),
form::select::Field => |c| theme::bs::form::select::setup(c),
form::Textarea => |c| theme::bs::form::textarea::setup(c),

View file

@ -1,7 +1,24 @@
//! Definiciones para renderizar imágenes ([`Image`]).
mod props;
pub use props::{Size, Source};
use pagetop::prelude::*;
mod component;
pub use component::Image;
pub use pagetop::base::component::image::{Image, Size, Source};
// **< Image SETUP >********************************************************************************
pub(crate) fn setup(image: &mut Image) {
match image.source() {
Source::Logo(_) | Source::Responsive(_) => {
image.alter_prop(PropsOp::replace_classes("image image-fluid", "img-fluid"));
}
Source::Thumbnail(_) => {
image.alter_prop(PropsOp::replace_classes(
"image image-thumbnail",
"img-thumbnail",
));
}
Source::Plain(_) => {
image.alter_prop(PropsOp::remove_classes("image"));
}
}
}

View file

@ -1,124 +0,0 @@
use pagetop::prelude::*;
use crate::theme::*;
/// Componente para renderizar una **imagen**.
///
/// A una imagen se le puede:
///
/// - Establecer su contenido a partir del origen definido en
/// [`image::Source`](crate::theme::bs::image::Source).
/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)), **borde**
/// ([`Border`](crate::theme::class::Border)) y **redondeo de esquinas**
/// ([`Rounded`](crate::theme::class::Rounded)).
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`Lc`].
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Image {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve las dimensiones de la imagen.
size: bs::image::Size,
/// Devuelve el origen de la imagen.
source: bs::image::Source,
/// Devuelve el texto alternativo localizado.
alternative: Attr<Lc>,
}
#[async_trait]
impl Component for Image {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
// Clases CSS por defecto para la imagen, según el origen seleccionado.
self.alter_prop(PropsOp::prepend_classes(self.source().to_class()));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let dimensions = self.size().to_style();
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
let is_decorative = alt_text.is_empty();
let source = match self.source() {
bs::image::Source::Logo(logo) => {
return Ok(html! {
span
(self.props())
style=[dimensions]
role=[(!is_decorative).then_some("img")]
aria-label=[(!is_decorative).then_some(alt_text)]
aria-hidden=[is_decorative.then_some("true")]
{
(logo.markup(cx))
}
});
}
bs::image::Source::Responsive(source) => Some(source),
bs::image::Source::Thumbnail(source) => Some(source),
bs::image::Source::Plain(source) => Some(source),
};
Ok(html! {
img
src=[source]
alt=(alt_text)
(self.props())
style=[dimensions] {}
})
}
}
impl Image {
/// Crea rápidamente una imagen especificando su origen.
pub fn with(source: bs::image::Source) -> Self {
Self::default().with_source(source)
}
// **< Image BUILDER >**************************************************************************
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
#[builder_fn]
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
self.props.alter_id(id);
self
}
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
///
/// También acepta clases predefinidas para:
///
/// - Establecer bordes ([`Border`]).
/// - Redondear las esquinas ([`Rounded`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
#[builder_fn]
pub fn with_size(mut self, size: bs::image::Size) -> Self {
self.size = size;
self
}
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
#[builder_fn]
pub fn with_source(mut self, source: bs::image::Source) -> Self {
self.source = source;
self
}
/// Define un *texto localizado* ([`Lc`]) alternativo para la imagen.
///
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
/// decorativa.
#[builder_fn]
pub fn with_alternative(mut self, alt: Lc) -> Self {
self.alternative.alter_value(alt);
self
}
}

View file

@ -1,126 +0,0 @@
use pagetop::prelude::*;
// **< Size >***************************************************************************************
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::bs::Image)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Size {
/// Ajuste automático por defecto.
///
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
#[default]
Auto,
/// Establece explícitamente el **ancho y alto** de la imagen.
///
/// Útil cuando se desea fijar ambas dimensiones de forma exacta. Ten en cuenta que la imagen
/// puede distorsionarse si no se mantiene la proporción original.
Dimensions(UnitValue, UnitValue),
/// Establece sólo el **ancho** de la imagen.
///
/// La altura se ajusta proporcionalmente de manera automática.
Width(UnitValue),
/// Establece sólo la **altura** de la imagen.
///
/// El ancho se ajusta proporcionalmente de manera automática.
Height(UnitValue),
/// Establece **el mismo valor** para el ancho y el alto de la imagen.
///
/// Práctico para forzar rápidamente un área cuadrada. Ten en cuenta que la imagen puede
/// distorsionarse si la original no es cuadrada.
Both(UnitValue),
}
impl Size {
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
#[inline]
pub fn to_style(self) -> Option<String> {
match self {
Self::Auto => None,
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
Self::Width(w) => Some(format!("width: {w};")),
Self::Height(h) => Some(format!("height: {h};")),
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
}
}
}
// **< Source >*************************************************************************************
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::bs::Image)).
#[derive(AutoDefault, Clone, Debug, PartialEq)]
pub enum Source {
/// Imagen con el logotipo de PageTop.
#[default]
Logo(PageTopSvg),
/// Imagen que se adapta automáticamente a su contenedor.
///
/// Lleva asociada la URL (o ruta) de la imagen.
Responsive(CowStr),
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
///
/// Lleva asociada la URL (o ruta) de la imagen.
Thumbnail(CowStr),
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
///
/// Lleva asociada la URL (o ruta) de la imagen.
Plain(CowStr),
}
impl Source {
const IMG_FLUID: &str = "img-fluid";
const IMG_THUMBNAIL: &str = "img-thumbnail";
/// Imagen con el logotipo de PageTop.
#[inline]
pub fn logo(svg: PageTopSvg) -> Self {
Self::Logo(svg)
}
/// Imagen responsive (`img-fluid`).
#[inline]
pub fn responsive(url: impl Into<CowStr>) -> Self {
Self::Responsive(url.into())
}
/// Imagen miniatura (`img-thumbnail`).
#[inline]
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
Self::Thumbnail(url.into())
}
/// Imagen sin clases adicionales.
#[inline]
pub fn plain(url: impl Into<CowStr>) -> Self {
Self::Plain(url.into())
}
/// Devuelve la clase base asociada a la imagen según la fuente.
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
Source::Plain(_) => "",
}
}
/// Añade la clase asociada al tipo de imagen a la cadena de clases.
#[inline]
pub fn push_to(&self, classes: &mut String) {
let s = self.as_str();
if s.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(s);
}
/// Devuelve la clase asociada al tipo de imagen.
pub fn to_class(&self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}