From f6c98100e1088f1cd1d2ea656d055307bcc59dc8 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Sat, 2 May 2026 08:16:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(bootsier):=20A=C3=B1ade=20botones?= =?UTF-8?q?=20con=20componente=20Button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mueve `ButtonAction`, `ButtonColor` y `ButtonSize` a `aux/button.rs`. Normaliza la documentación de `aux/border.rs` y `aux/color.rs`. --- extensions/pagetop-bootsier/src/theme.rs | 4 + extensions/pagetop-bootsier/src/theme/aux.rs | 2 +- .../pagetop-bootsier/src/theme/aux/border.rs | 6 +- .../pagetop-bootsier/src/theme/aux/button.rs | 39 +++- .../pagetop-bootsier/src/theme/aux/color.rs | 12 +- .../pagetop-bootsier/src/theme/button.rs | 213 ++++++++++++++++++ 6 files changed, 258 insertions(+), 18 deletions(-) create mode 100644 extensions/pagetop-bootsier/src/theme/button.rs diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index 7464caf5..39ed51cb 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -9,6 +9,10 @@ pub use aux::*; pub mod classes; +// Button. +mod button; +pub use button::Button; + // Container. pub mod container; #[doc(inline)] diff --git a/extensions/pagetop-bootsier/src/theme/aux.rs b/extensions/pagetop-bootsier/src/theme/aux.rs index 99431fe3..e5f0a82f 100644 --- a/extensions/pagetop-bootsier/src/theme/aux.rs +++ b/extensions/pagetop-bootsier/src/theme/aux.rs @@ -17,4 +17,4 @@ mod rounded; pub use rounded::RoundedRadius; mod button; -pub use button::{ButtonColor, ButtonSize}; +pub use button::{ButtonAction, ButtonColor, ButtonSize}; diff --git a/extensions/pagetop-bootsier/src/theme/aux/border.rs b/extensions/pagetop-bootsier/src/theme/aux/border.rs index 44d24b0e..bf81bced 100644 --- a/extensions/pagetop-bootsier/src/theme/aux/border.rs +++ b/extensions/pagetop-bootsier/src/theme/aux/border.rs @@ -2,15 +2,15 @@ use pagetop::prelude::*; use crate::theme::aux::Color; -/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)). +/// Esquema de color para los bordes ([`classes::Border`](crate::theme::classes::Border)). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum BorderColor { /// No define ninguna clase. #[default] Default, - /// Genera internamente clases `border-{color}`. + /// Genera la clase `border-{color}`. Theme(Color), - /// Genera internamente clases `border-{color}-subtle` (un tono suavizado del color). + /// Genera la clase `border-{color}-subtle` (un tono suavizado del color). Subtle(Color), /// Color negro. Black, diff --git a/extensions/pagetop-bootsier/src/theme/aux/button.rs b/extensions/pagetop-bootsier/src/theme/aux/button.rs index 876e8f87..1464e288 100644 --- a/extensions/pagetop-bootsier/src/theme/aux/button.rs +++ b/extensions/pagetop-bootsier/src/theme/aux/button.rs @@ -2,17 +2,42 @@ use pagetop::prelude::*; use crate::theme::aux::Color; +// **< ButtonAction >********************************************************************************* + +/// Comportamiento de un [`Button`](crate::theme::Button) al activarse. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum ButtonAction { + /// Envía un formulario al servidor. Es el **tipo por defecto**. + #[default] + Submit, + /// Restablece todos los campos de un formulario a sus valores iniciales. + Reset, + /// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse + /// mediante JavaScript. + Plain, +} + +impl std::fmt::Display for ButtonAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ButtonAction::Submit => "submit", + ButtonAction::Reset => "reset", + ButtonAction::Plain => "button", + }) + } +} + // **< ButtonColor >******************************************************************************** -/// Variantes de color `btn-*` para botones. +/// Esquema de color para [`Button`](crate::theme::Button). #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ButtonColor { /// No define ninguna clase. #[default] Default, - /// Genera internamente clases `btn-{color}` (botón relleno). + /// Genera la clase `btn-{color}` (botón sólido). Background(Color), - /// Genera `btn-outline-{color}` (fondo transparente y contorno con borde). + /// Genera la clase `btn-outline-{color}` (fondo transparente con contorno coloreado). Outline(Color), /// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto. Link, @@ -33,7 +58,6 @@ impl ButtonColor { classes.push(' '); } match self { - Self::Default => {} Self::Background(c) => { classes.push_str(Self::BTN_PREFIX); classes.push_str(c.as_str()); @@ -42,9 +66,8 @@ impl ButtonColor { classes.push_str(Self::BTN_OUTLINE_PREFIX); classes.push_str(c.as_str()); } - Self::Link => { - classes.push_str(Self::BTN_LINK); - } + Self::Link => classes.push_str(Self::BTN_LINK), + Self::Default => unreachable!(), } } @@ -82,7 +105,7 @@ pub enum ButtonSize { Default, /// Botón compacto. Small, - /// Botón destacado/grande. + /// Botón grande. Large, } diff --git a/extensions/pagetop-bootsier/src/theme/aux/color.rs b/extensions/pagetop-bootsier/src/theme/aux/color.rs index a56d9db2..c408aadc 100644 --- a/extensions/pagetop-bootsier/src/theme/aux/color.rs +++ b/extensions/pagetop-bootsier/src/theme/aux/color.rs @@ -169,7 +169,7 @@ impl Opacity { // **< ColorBg >************************************************************************************ -/// Colores `bg-*` para el fondo. +/// Esquema de color para el fondo. #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ColorBg { /// No define ninguna clase. @@ -181,9 +181,9 @@ pub enum ColorBg { BodySecondary, /// Fondo predefinido del tema (`bg-body-tertiary`). BodyTertiary, - /// Genera internamente clases `bg-{color}` (p. ej., `bg-primary`). + /// Genera la clase `bg-{color}` (p. ej., `bg-primary`). Theme(Color), - /// Genera internamente clases `bg-{color}-subtle` (un tono suavizado del color). + /// Genera la clase `bg-{color}-subtle` (un tono suavizado del color). Subtle(Color), /// Color negro. Black, @@ -253,7 +253,7 @@ impl ColorBg { // **< ColorText >********************************************************************************** -/// Colores `text-*` para el texto. +/// Esquema de color para el texto. #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum ColorText { /// No define ninguna clase. @@ -267,9 +267,9 @@ pub enum ColorText { BodySecondary, /// Color predefinido del tema (`text-body-tertiary`). BodyTertiary, - /// Genera internamente clases `text-{color}`. + /// Genera la clase `text-{color}`. Theme(Color), - /// Genera internamente clases `text-{color}-emphasis` (mayor contraste acorde al tema). + /// Genera la clase `text-{color}-emphasis` (mayor contraste acorde al tema). Emphasis(Color), /// Color negro. Black, diff --git a/extensions/pagetop-bootsier/src/theme/button.rs b/extensions/pagetop-bootsier/src/theme/button.rs new file mode 100644 index 00000000..48eb7283 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/button.rs @@ -0,0 +1,213 @@ +use pagetop::prelude::*; + +use crate::theme::{ButtonAction, ButtonColor, ButtonSize}; + +/// Componente para crear un **botón**. +/// +/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`, +/// `reset` y botón genérico) y con la variedad de estilos del tema a través de [`ButtonColor`] y +/// [`ButtonSize`]. +/// +/// El comportamiento del botón se establece al crearlo: +/// +/// - [`Button::submit()`]: botón de envío (por defecto). +/// - [`Button::reset()`]: botón de restablecimiento de valores. +/// - [`Button::plain()`]: botón genérico sin comportamiento predeterminado. +/// +/// El botón puede usarse dentro o fuera de un formulario. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::prelude::*; +/// let save = Button::submit(L10n::n("Save")) +/// .with_color(ButtonColor::Background(Color::Primary)); +/// +/// let cancel = Button::plain(L10n::n("Cancel")) +/// .with_color(ButtonColor::Outline(Color::Secondary)); +/// +/// let clear = Button::reset(L10n::n("Clear")) +/// .with_size(ButtonSize::Small); +/// ``` +/// +/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del +/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar +/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// #[serde(default)] +/// action: Option, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Button { + #[getters(skip)] + id: AttrId, + /// Devuelve las clases CSS del botón. + classes: Classes, + /// Devuelve el comportamiento del botón al activarse. + kind: ButtonAction, + /// Devuelve el esquema de color del botón. + color: ButtonColor, + /// Devuelve el tamaño visual del botón. + size: ButtonSize, + /// Devuelve el nombre del botón. + name: AttrName, + /// Devuelve el valor del botón. + value: AttrValue, + /// Devuelve la etiqueta del botón. + label: Attr, + /// Devuelve si el botón recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si el botón está deshabilitado. + disabled: bool, +} + +impl Component for Button { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + fn setup(&mut self, _cx: &Context) { + let mut classes = "btn".to_string(); + (*self.color()).push_class(&mut classes); + (*self.size()).push_class(&mut classes); + self.alter_classes(ClassesOp::Prepend, classes); + } + + fn prepare(&self, cx: &mut Context) -> Result { + Ok(html! { + button + id=[self.id()] + type=(self.kind()) + class=[self.classes().get()] + name=[self.name().get()] + value=[self.value().get()] + autofocus[*self.autofocus()] + disabled[*self.disabled()] + { + @if let Some(label) = self.label().lookup(cx) { + (label) + } + } + }) + } +} + +impl Button { + /// Crea un botón de **envío** (`type="submit"`). + /// + /// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los + /// datos al servidor. + pub fn submit(label: L10n) -> Self { + Self { + kind: ButtonAction::Submit, + label: Attr::some(label), + ..Default::default() + } + } + + /// Crea un botón de **restablecimiento** (`type="reset"`). + /// + /// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales. + pub fn reset(label: L10n) -> Self { + Self { + kind: ButtonAction::Reset, + label: Attr::some(label), + ..Default::default() + } + } + + /// Crea un **botón genérico** (`type="button"`). + /// + /// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede + /// definirse mediante JavaScript. + pub fn plain(label: L10n) -> Self { + Self { + kind: ButtonAction::Plain, + label: Attr::some(label), + ..Default::default() + } + } + + // **< Button BUILDER >************************************************************************* + + /// Establece el identificador único (`id`) del botón. + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); + self + } + + /// Modifica la lista de clases CSS aplicadas al botón. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_classes(op, classes); + self + } + + /// Establece el esquema de color del botón. + /// + /// Usa [`ButtonColor::Background`] para botones sólidos o [`ButtonColor::Outline`] para + /// variantes con contorno. + #[builder_fn] + pub fn with_color(mut self, color: ButtonColor) -> Self { + self.color = color; + self + } + + /// Establece el tamaño visual del botón. + #[builder_fn] + pub fn with_size(mut self, size: ButtonSize) -> Self { + self.size = size; + self + } + + /// Establece el nombre del botón (atributo `name`). + /// + /// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par + /// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + /// Establece el valor del botón (atributo `value`). + /// + /// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón + /// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario. + #[builder_fn] + pub fn with_value(mut self, value: impl AsRef) -> Self { + self.value.alter_str(value); + self + } + + /// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla). + #[builder_fn] + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece si el botón recibe el foco automáticamente al cargar la página. + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + /// Establece si el botón está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +}