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

`Container` y `Kind` pasan a `pagetop::base::component` como componente
genérico reutilizable por cualquier tema. Bootsier conserva `Width` y
añade el *trait* `ContainerBootsier`, que guarda el ancho como valor
extra en `Props`.
This commit is contained in:
Manuel Cillero 2026-07-09 21:44:52 +02:00
parent 654618aa68
commit 771f61bf29
15 changed files with 268 additions and 274 deletions

View file

@ -144,6 +144,7 @@ impl Theme for Bootsier {
) -> Option<Result<Markup, ComponentError>> { ) -> Option<Result<Markup, ComponentError>> {
setup_component!(component, { setup_component!(component, {
Button => |c| theme::bs::button::setup(c), Button => |c| theme::bs::button::setup(c),
Container => |c| theme::bs::container::setup(c),
form::input::Field => |c| theme::bs::form::input::setup(c), form::input::Field => |c| theme::bs::form::input::setup(c),
form::select::Field => |c| theme::bs::form::select::setup(c), form::select::Field => |c| theme::bs::form::select::setup(c),
form::Textarea => |c| theme::bs::form::textarea::setup(c), form::Textarea => |c| theme::bs::form::textarea::setup(c),
@ -167,17 +168,17 @@ impl Theme for Bootsier {
.alter_assets(AssetsOp::AddStyleSheet( .alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/bootsier/css/bootsier.min.css") StyleSheet::from("/bootsier/css/bootsier.min.css")
.with_version(ADMINLTE_VERSION) .with_version(ADMINLTE_VERSION)
.with_weight(-90), .with_weight(-99),
)) ))
.alter_assets(AssetsOp::AddJavaScript( .alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js") JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
.with_version(BOOTSTRAP_VERSION) .with_version(BOOTSTRAP_VERSION)
.with_weight(-90), .with_weight(-99),
)) ))
.alter_assets(AssetsOp::AddJavaScript( .alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/bootsier/js/bootsier.extended.min.js") JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
.with_version(ADMINLTE_VERSION) .with_version(ADMINLTE_VERSION)
.with_weight(-90), .with_weight(-99),
)) ))
.alter_child_in( .alter_child_in(
&DefaultRegion::Footer, &DefaultRegion::Footer,

View file

@ -1,14 +1,4 @@
//! Tipos y componentes disponibles. //! Componentes proporcionados por el tema.
//!
//! A continuación, el apartado **Modules** incluye las definiciones necesarias para los componentes
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
//! auxiliares del tema utilizados en clases y componentes.
mod attrs;
pub use attrs::*;
mod classes;
pub use classes::*;
// Button. // Button.
mod button; mod button;
@ -18,6 +8,8 @@ pub use button::{Button, ButtonAction};
pub mod container; pub mod container;
#[doc(inline)] #[doc(inline)]
pub use container::Container; pub use container::Container;
#[doc(inline)]
pub use container::ContainerBootsier;
// Dropdown. // Dropdown.
pub mod dropdown; pub mod dropdown;

View file

@ -1,14 +1,99 @@
//! Definiciones para crear contenedores de componentes ([`Container`]). //! Definiciones para crear contenedores de componentes ([`Container`]).
//!
//! Cada contenedor envuelve contenido usando la etiqueta semántica indicada por
//! [`container::Kind`](crate::theme::bs::container::Kind).
//!
//! Con [`container::Width`](crate::theme::bs::container::Width) se puede definir el ancho y el
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
//! fondo, texto, borde o esquinas redondeadas.
mod props; use pagetop::prelude::*;
pub use props::{Kind, Width};
mod component; use crate::theme::*;
pub use component::Container;
pub use pagetop::base::component::container::{Container, Kind};
const EXTRA_WIDTH: &str = "bootsier.container.width";
/// Extensión de Bootsier para [`Container`].
///
/// Permite definir el comportamiento del ancho del contenedor usando el método
/// [`with_width()`](Self::with_width). También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`Background`](crate::theme::class::Background)).
/// - Definir la apariencia del texto ([`Text`](crate::theme::class::Text)).
/// - Establecer bordes ([`Border`](crate::theme::class::Border)).
/// - Redondear las esquinas ([`Rounded`](crate::theme::class::Rounded)).
///
/// ```rust,no_run
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let main = bs::Container::main()
/// .with_id("main-page")
/// .with_width(bs::container::Width::From(token::BreakPoint::LG))
/// .with_prop(PropsOp::add_classes(class::Background::with(token::Color::Light)))
/// .with_prop(PropsOp::add_classes(class::Text::with(token::Color::Dark)))
/// .with_prop(PropsOp::add_classes(class::Border::with(token::ScaleSize::One)))
/// .with_prop(PropsOp::add_classes(class::Rounded::with(token::RoundedRadius::Default)));
/// ```
pub trait ContainerBootsier {
/// Establece el comportamiento del ancho para el contenedor.
///
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de
/// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo
/// explícito. Ver [`Width`] para las variantes disponibles.
fn with_width(self, width: Width) -> Self;
}
impl ContainerBootsier for Container {
fn with_width(self, width: Width) -> Self {
self.with_prop(PropsOp::set_extra(EXTRA_WIDTH, width))
}
}
// **< Width >**************************************************************************************
/// Define cómo se comporta el ancho de un contenedor ([`Container`]).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Width {
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
#[default]
Default,
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
/// ese punto de ruptura ocupa el 100% del ancho disponible.
From(token::BreakPoint),
/// Ocupa el 100% del ancho disponible siempre.
Fluid,
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
FluidMax(UnitValue),
}
impl Width {
const CONTAINER: &str = "container";
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
#[inline]
pub fn push_to(self, classes: &mut String) {
match self {
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
}
}
}
/// Devuelve la clase asociada al ancho del contenedor.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}
// **< Container SETUP >****************************************************************************
pub(crate) fn setup(container: &mut Container) {
let width = container.props().extra_or(EXTRA_WIDTH, Width::default());
container.alter_prop(PropsOp::prepend_classes(width.to_class()));
if let Width::FluidMax(w) = width
&& w.is_measurable()
{
container.alter_prop(PropsOp::add_style("max-width", w.to_string()));
}
}

View file

@ -1,172 +0,0 @@
use pagetop::prelude::*;
use crate::theme::*;
/// Componente para crear un **contenedor de componentes**
/// ([`container`](crate::theme::bs::container)).
///
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
/// los tipos definidos en [`container::Kind`](crate::theme::bs::container::Kind).
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop_bootsier::theme::*;
///
/// let main = bs::Container::main()
/// .with_id("main-page")
/// .with_width(bs::container::Width::From(token::BreakPoint::LG));
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Container {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve el tipo semántico del contenedor.
container_kind: bs::container::Kind,
/// Devuelve el comportamiento para el ancho del contenedor.
container_width: bs::container::Width,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
#[async_trait]
impl Component for Container {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
fn setup(&mut self, _cx: &Context) {
self.alter_prop(PropsOp::prepend_classes(self.container_width().to_class()));
}
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx).await;
if output.is_empty() {
return Ok(html! {});
}
let style = match self.container_width() {
bs::container::Width::FluidMax(w) if w.is_measurable() => {
Some(util::join!("max-width: ", w.to_string(), ";"))
}
_ => None,
};
Ok(match self.container_kind() {
bs::container::Kind::Default => html! {
div (self.props()) style=[style] {
(output)
}
},
bs::container::Kind::Main => html! {
main (self.props()) style=[style] {
(output)
}
},
bs::container::Kind::Header => html! {
header (self.props()) style=[style] {
(output)
}
},
bs::container::Kind::Footer => html! {
footer (self.props()) style=[style] {
(output)
}
},
bs::container::Kind::Section => html! {
section (self.props()) style=[style] {
(output)
}
},
bs::container::Kind::Article => html! {
article (self.props()) style=[style] {
(output)
}
},
})
}
}
impl Container {
/// Crea un contenedor de tipo `Main` (`<main>`).
pub fn main() -> Self {
Self {
container_kind: bs::container::Kind::Main,
..Default::default()
}
}
/// Crea un contenedor de tipo `Header` (`<header>`).
pub fn header() -> Self {
Self {
container_kind: bs::container::Kind::Header,
..Default::default()
}
}
/// Crea un contenedor de tipo `Footer` (`<footer>`).
pub fn footer() -> Self {
Self {
container_kind: bs::container::Kind::Footer,
..Default::default()
}
}
/// Crea un contenedor de tipo `Section` (`<section>`).
pub fn section() -> Self {
Self {
container_kind: bs::container::Kind::Section,
..Default::default()
}
}
/// Crea un contenedor de tipo `Article` (`<article>`).
pub fn article() -> Self {
Self {
container_kind: bs::container::Kind::Article,
..Default::default()
}
}
// **< Container 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:
///
/// - Modificar el color de fondo ([`Background`]).
/// - Definir la apariencia del texto ([`Text`]).
/// - Establecer bordes ([`Border`]).
/// - Redondear las esquinas ([`Rounded`]).
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Establece el comportamiento del ancho para el contenedor.
#[builder_fn]
pub fn with_width(mut self, width: bs::container::Width) -> Self {
self.container_width = width;
self
}
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
/// una operación [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.children.alter_child(op.into());
self
}
}

View file

@ -1,67 +0,0 @@
use pagetop::prelude::*;
use crate::theme::*;
// **< Kind >***************************************************************************************
/// Tipo de contenedor ([`Container`](crate::theme::bs::Container)).
///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// Contenedor genérico (`<div>`).
#[default]
Default,
/// Contenido principal de la página (`<main>`).
Main,
/// Encabezado de la página o de sección (`<header>`).
Header,
/// Pie de la página o de sección (`<footer>`).
Footer,
/// Sección de contenido (`<section>`).
Section,
/// Artículo de contenido (`<article>`).
Article,
}
// **< Width >**************************************************************************************
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::bs::Container)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Width {
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
#[default]
Default,
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
/// ese punto de ruptura ocupa el 100% del ancho disponible.
From(token::BreakPoint),
/// Ocupa el 100% del ancho disponible siempre.
Fluid,
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
FluidMax(UnitValue),
}
impl Width {
const CONTAINER: &str = "container";
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
#[inline]
pub fn push_to(self, classes: &mut String) {
match self {
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
}
}
}
/// Devuelve la clase asociada al ancho del contenedor.
pub fn to_class(self) -> String {
let mut class = String::new();
self.push_to(&mut class);
class
}
}

View file

@ -3,7 +3,7 @@ use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
/// Componente para crear un **menú desplegable** ([`dropdown`](crate::theme::bs::dropdown)). /// Componente para crear un **menú desplegable**.
/// ///
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split)) /// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
/// con un menú desplegable de elementos [`dropdown::Item`](crate::theme::bs::dropdown::Item), que /// con un menú desplegable de elementos [`dropdown::Item`](crate::theme::bs::dropdown::Item), que

View file

@ -13,11 +13,17 @@ pub use pagetop::base::component::form::check;
pub use pagetop::base::component::form::radio; pub use pagetop::base::component::form::radio;
pub mod select; pub mod select;
#[doc(inline)]
pub use select::SelectBootsier;
pub mod input; pub mod input;
#[doc(inline)]
pub use input::InputBootsier;
pub mod textarea; pub mod textarea;
pub use textarea::Textarea; pub use textarea::Textarea;
#[doc(inline)]
pub use textarea::TextareaBootsier;
pub use pagetop::base::component::form::Range; pub use pagetop::base::component::form::Range;

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
use crate::theme::*; use crate::theme::*;
/// Componente para renderizar una **imagen** ([`image`](crate::theme::bs::image)). /// Componente para renderizar una **imagen**.
/// ///
/// A una imagen se le puede: /// A una imagen se le puede:
/// ///

View file

@ -2,7 +2,7 @@ use pagetop::prelude::*;
use crate::theme::*; use crate::theme::*;
/// Componente para crear un **menú** ([`nav`](crate::theme::bs::nav)). /// Componente para crear un **menú**.
/// ///
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes /// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
/// ([`nav::Kind`](crate::theme::bs::nav::Kind)) como *pestañas* (`Tabs`), *botones* (`Pills`) o /// ([`nav::Kind`](crate::theme::bs::nav::Kind)) como *pestañas* (`Tabs`), *botones* (`Pills`) o

View file

@ -6,7 +6,7 @@ use crate::theme::*;
const TOGGLE_COLLAPSE: &str = "collapse"; const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas"; const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// Componente para crear una **barra de navegación** ([`navbar`](crate::theme::bs::navbar)). /// Componente para crear una **barra de navegación**.
/// ///
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con /// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por /// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por

View file

@ -3,7 +3,7 @@ use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
use crate::theme::*; use crate::theme::*;
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`](crate::theme::bs::offcanvas)). /// Componente para crear un **panel lateral deslizante**.
/// ///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes /// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
/// características principales: /// características principales:

View file

@ -6,6 +6,10 @@ pub use block::Block;
mod button; mod button;
pub use button::{Button, ButtonAction}; pub use button::{Button, ButtonAction};
pub mod container;
#[doc(inline)]
pub use container::Container;
pub mod form; pub mod form;
#[doc(inline)] #[doc(inline)]
pub use form::Form; pub use form::Form;

View file

@ -0,0 +1,145 @@
//! Definiciones para crear contenedores de componentes ([`Container`]).
use crate::prelude::*;
// **< Kind >***************************************************************************************
/// Tipo de contenedor (`Container`).
///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// Contenedor genérico (`<div>`).
#[default]
Default,
/// Contenido principal de la página (`<main>`).
Main,
/// Encabezado de la página o de sección (`<header>`).
Header,
/// Pie de la página o de sección (`<footer>`).
Footer,
/// Sección de contenido (`<section>`).
Section,
/// Artículo de contenido (`<article>`).
Article,
}
// **< Container >**********************************************************************************
/// Componente para crear un **contenedor de componentes**.
///
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
/// los tipos definidos en [`Kind`].
///
/// Si no contiene elementos, el componente **no se renderiza**.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
///
/// let main = Container::main().with_id("main-page");
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Container {
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
props: Props,
/// Devuelve el tipo semántico del contenedor.
kind: Kind,
/// Devuelve la lista de componentes (`children`) del contenedor.
children: Children,
}
#[async_trait]
impl Component for Container {
fn new() -> Self {
Self::default()
}
fn id(&self) -> Option<String> {
self.props.get_id()
}
#[rustfmt::skip]
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let output = self.children().render(cx).await;
if output.is_empty() {
return Ok(html! {});
}
Ok(match self.kind() {
Kind::Default => html! { div (self.props()) { (output) } },
Kind::Main => html! { main (self.props()) { (output) } },
Kind::Header => html! { header (self.props()) { (output) } },
Kind::Footer => html! { footer (self.props()) { (output) } },
Kind::Section => html! { section (self.props()) { (output) } },
Kind::Article => html! { article (self.props()) { (output) } },
})
}
}
impl Container {
/// Crea un contenedor de tipo `Main` (`<main>`).
pub fn main() -> Self {
Self {
kind: Kind::Main,
..Default::default()
}
}
/// Crea un contenedor de tipo `Header` (`<header>`).
pub fn header() -> Self {
Self {
kind: Kind::Header,
..Default::default()
}
}
/// Crea un contenedor de tipo `Footer` (`<footer>`).
pub fn footer() -> Self {
Self {
kind: Kind::Footer,
..Default::default()
}
}
/// Crea un contenedor de tipo `Section` (`<section>`).
pub fn section() -> Self {
Self {
kind: Kind::Section,
..Default::default()
}
}
/// Crea un contenedor de tipo `Article` (`<article>`).
pub fn article() -> Self {
Self {
kind: Kind::Article,
..Default::default()
}
}
// **< Container 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.
#[builder_fn]
pub fn with_prop(mut self, op: PropsOp) -> Self {
self.props.alter_prop(op);
self
}
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
/// una operación [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.children.alter_child(op.into());
self
}
}

View file

@ -1,4 +1,4 @@
//! Componentes y tipos para crear formularios HTML ([`Form`]). //! Definiciones para crear formularios ([`Form`]).
mod props; mod props;
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method}; pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};

View file

@ -2,7 +2,7 @@ use crate::prelude::*;
use crate::base::component::form; use crate::base::component::form;
/// Componente para crear un **formulario** HTML ([`form`]). /// Componente para crear un **formulario**.
/// ///
/// Renderiza un formulario estándar con soporte para los atributos más habituales: /// Renderiza un formulario estándar con soporte para los atributos más habituales:
/// ///