Compare commits

..

No commits in common. "e5a43832abff08c0b2c769b49b0a7d1b63646448" and "28f2703ef1ea173a1625473f842aa8927a368f48" have entirely different histories.

29 changed files with 484 additions and 1190 deletions

View file

@ -1,8 +1,5 @@
# Dropdown
dropdown_toggle = Toggle Dropdown
# Offcanvas # Offcanvas
offcanvas_close = Close close = Close
# Navbar # Navbar
toggle = Toggle navigation toggle = Toggle navigation

View file

@ -1,8 +1,5 @@
# Dropdown
dropdown_toggle = Mostrar/ocultar menú
# Offcanvas # Offcanvas
offcanvas_close = Cerrar close = Cerrar
# Navbar # Navbar
toggle = Mostrar/ocultar navegación toggle = Mostrar/ocultar navegación

View file

@ -4,7 +4,7 @@ pub mod aux;
// Container. // Container.
mod container; mod container;
pub use container::{Container, ContainerType, ContainerWidth}; pub use container::{Container, ContainerType};
// Dropdown. // Dropdown.
pub mod dropdown; pub mod dropdown;
@ -21,6 +21,7 @@ pub mod navbar;
pub use navbar::{Navbar, NavbarToggler}; pub use navbar::{Navbar, NavbarToggler};
// Offcanvas. // Offcanvas.
pub mod offcanvas; mod offcanvas;
#[doc(inline)] pub use offcanvas::{
pub use offcanvas::Offcanvas; Offcanvas, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility,
};

View file

@ -1,11 +1,11 @@
//! Colección de elementos auxiliares de Bootstrap para Bootsier. //! Coleción de elementos auxiliares de Bootstrap para Bootsier.
mod breakpoint; mod breakpoint;
pub use breakpoint::BreakPoint; pub use breakpoint::BreakPoint;
mod color; mod color;
pub use color::Color; pub use color::Color;
pub use color::{BgColor, BorderColor, ButtonColor, TextColor}; pub use color::{BgColor, BorderColor, TextColor};
mod opacity; mod opacity;
pub use opacity::Opacity; pub use opacity::Opacity;
@ -16,6 +16,3 @@ pub use border::{Border, BorderSize};
mod rounded; mod rounded;
pub use rounded::{Rounded, RoundedRadius}; pub use rounded::{Rounded, RoundedRadius};
mod size;
pub use size::ButtonSize;

View file

@ -7,6 +7,10 @@ use std::fmt;
/// - `"sm"`, `"md"`, `"lg"`, `"xl"` o `"xxl"` para los puntos de ruptura `SM`, `MD`, `LG`, `XL` o /// - `"sm"`, `"md"`, `"lg"`, `"xl"` o `"xxl"` para los puntos de ruptura `SM`, `MD`, `LG`, `XL` o
/// `XXL`, respectivamente. /// `XXL`, respectivamente.
/// - `""` (cadena vacía) para `None`. /// - `""` (cadena vacía) para `None`.
/// - `"fluid"` para las variantes `Fluid` y `FluidMax(_)`, útil para modelar `container-fluid` en
/// [`Container`](crate::theme::Container). Se debe tener en cuenta que `"fluid"` **no** es un
/// sufijo de *breakpoint*. Para construir clases válidas con prefijo (p. ej., `"col-md"`), se
/// recomienda usar `to_class()` (Self::to_class) o `try_class()` (Self::try_class).
/// ///
/// # Ejemplos /// # Ejemplos
/// ///
@ -14,14 +18,13 @@ use std::fmt;
/// # use pagetop_bootsier::prelude::*; /// # use pagetop_bootsier::prelude::*;
/// assert_eq!(BreakPoint::MD.to_string(), "md"); /// assert_eq!(BreakPoint::MD.to_string(), "md");
/// assert_eq!(BreakPoint::None.to_string(), ""); /// assert_eq!(BreakPoint::None.to_string(), "");
/// assert_eq!(BreakPoint::Fluid.to_string(), "fluid");
/// ///
/// // Forma correcta para clases con prefijo: /// // Forma correcta para clases con prefijo:
/// assert_eq!(BreakPoint::MD.to_class("col"), "col-md"); /// //assert_eq!(BreakPoint::MD.to_class("col"), "col-md");
/// assert_eq!(BreakPoint::None.to_class("offcanvas"), "offcanvas"); /// //assert_eq!(BreakPoint::Fluid.to_class("offcanvas"), "offcanvas");
///
/// assert_eq!(BreakPoint::XXL.try_class("col"), Some("col-xxl".to_string()));
/// assert_eq!(BreakPoint::None.try_class("offcanvas"), None);
/// ``` /// ```
#[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum BreakPoint { pub enum BreakPoint {
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical. /// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
@ -37,6 +40,11 @@ pub enum BreakPoint {
XL, XL,
/// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes. /// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes.
XXL, XXL,
/// Para [`Container`](crate::theme::Container), ocupa el 100% del ancho del dispositivo.
Fluid,
/// Para [`Container`](crate::theme::Container), ocupa el 100% del ancho del dispositivo hasta
/// un ancho máximo indicado.
FluidMax(UnitValue)
} }
impl BreakPoint { impl BreakPoint {
@ -45,7 +53,7 @@ impl BreakPoint {
/// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`; y `false` en otro caso. /// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`; y `false` en otro caso.
#[inline] #[inline]
pub const fn is_breakpoint(&self) -> bool { pub const fn is_breakpoint(&self) -> bool {
!matches!(self, Self::None) matches!(self, Self::SM | Self::MD | Self::LG | Self::XL | Self::XXL)
} }
/// Genera un nombre de clase CSS basado en el punto de ruptura. /// Genera un nombre de clase CSS basado en el punto de ruptura.
@ -57,12 +65,11 @@ impl BreakPoint {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust /// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let breakpoint = BreakPoint::MD; /// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.to_class("col"); /// let class = breakpoint.to_class("col");
/// assert_eq!(class, "col-md".to_string()); /// assert_eq!(class, "col-md".to_string());
/// ///
/// let breakpoint = BreakPoint::None; /// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.to_class("offcanvas"); /// let class = breakpoint.to_class("offcanvas");
/// assert_eq!(class, "offcanvas".to_string()); /// assert_eq!(class, "offcanvas".to_string());
/// ``` /// ```
@ -84,13 +91,12 @@ impl BreakPoint {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust /// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let breakpoint = BreakPoint::MD; /// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.try_class("col"); /// let class = breakpoint.try_class("col");
/// assert_eq!(class, Some("col-md".to_string())); /// assert_eq!(class, Some("col-md".to_string()));
/// ///
/// let breakpoint = BreakPoint::None; /// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.try_class("navbar-expand"); /// let class = breakpoint.try_class("navbar-expanded");
/// assert_eq!(class, None); /// assert_eq!(class, None);
/// ``` /// ```
#[inline] #[inline]
@ -113,6 +119,9 @@ impl fmt::Display for BreakPoint {
Self::LG => f.write_str("lg"), Self::LG => f.write_str("lg"),
Self::XL => f.write_str("xl"), Self::XL => f.write_str("xl"),
Self::XXL => f.write_str("xxl"), Self::XXL => f.write_str("xxl"),
// Devuelven "fluid" (para modelar `container-fluid`).
Self::Fluid => f.write_str("fluid"),
Self::FluidMax(_) => f.write_str("fluid"),
} }
} }
} }

View file

@ -110,35 +110,6 @@ impl fmt::Display for BorderColor {
} }
} }
// **< ButtonColor >********************************************************************************
/// Variantes de color (`btn-*`) para **botones**.
///
/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases).
/// - `Background(Color)` genera `btn-{color}` (botón relleno).
/// - `Outline(Color)` genera `btn-outline-{color}` (contorno: texto y borde, fondo transparente).
/// - `Link` aplica estilo de enlace (`btn-link`), sin caja ni fondo, heredando el color de texto.
#[derive(AutoDefault)]
pub enum ButtonColor {
#[default]
Default,
Background(Color),
Outline(Color),
Link,
}
#[rustfmt::skip]
impl fmt::Display for ButtonColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => Ok(()),
Self::Background(c) => write!(f, "btn-{c}"),
Self::Outline(c) => write!(f, "btn-outline-{c}"),
Self::Link => f.write_str("btn-link"),
}
}
}
// **< TextColor >********************************************************************************** // **< TextColor >**********************************************************************************
/// Colores de texto y fondos de texto (`text-*`). /// Colores de texto y fondos de texto (`text-*`).

View file

@ -1,31 +0,0 @@
use pagetop::prelude::*;
use std::fmt;
// **< ButtonSize >*********************************************************************************
/// Tamaño visual de un botón.
///
/// Controla la escala del botón según el diseño del tema:
///
/// - `Default`, tamaño por defecto del tema (no añade clase).
/// - `Small`, botón compacto.
/// - `Large`, botón destacado/grande.
#[derive(AutoDefault)]
pub enum ButtonSize {
#[default]
Default,
Small,
Large,
}
#[rustfmt::skip]
impl fmt::Display for ButtonSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => Ok(()),
Self::Small => f.write_str("btn-sm"),
Self::Large => f.write_str("btn-lg"),
}
}
}

View file

@ -2,12 +2,11 @@ use pagetop::prelude::*;
use crate::prelude::*; use crate::prelude::*;
// **< ContainerType >******************************************************************************
/// Tipo de contenedor ([`Container`]). /// Tipo de contenedor ([`Container`]).
/// ///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API /// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores. /// común a todos los contenedores.
#[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum ContainerType { pub enum ContainerType {
/// Contenedor genérico (`<div>`). /// Contenedor genérico (`<div>`).
@ -25,27 +24,7 @@ pub enum ContainerType {
Article, Article,
} }
// **< ContainerWidth >***************************************************************************** /// Componente genérico para crear un contenedor de componentes.
/// Define el comportamiento para ajustar el ancho de un contenedor ([`Container`]).
#[derive(AutoDefault)]
pub enum ContainerWidth {
/// 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(BreakPoint),
/// Ocupa el 100% del ancho disponible siempre.
Fluid,
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
FluidMax(UnitValue),
}
// **< Container >**********************************************************************************
/// Componente para crear un **contenedor de componentes**.
/// ///
/// Envuelve el contenido con la etiqueta HTML indicada por [`ContainerType`]. Sólo se renderiza si /// Envuelve el contenido con la etiqueta HTML indicada por [`ContainerType`]. Sólo se renderiza si
/// existen componentes hijos (*children*). /// existen componentes hijos (*children*).
@ -54,13 +33,13 @@ pub enum ContainerWidth {
pub struct Container { pub struct Container {
id : AttrId, id : AttrId,
classes : AttrClasses, classes : AttrClasses,
container_type : ContainerType, container_type: ContainerType,
container_width: ContainerWidth, breakpoint : BreakPoint,
children : Children,
bg_color : BgColor, bg_color : BgColor,
text_color : TextColor, text_color : TextColor,
border : Border, border : Border,
rounded : Rounded, rounded : Rounded,
children : Children,
} }
impl Component for Container { impl Component for Container {
@ -76,16 +55,7 @@ impl Component for Container {
self.alter_classes( self.alter_classes(
ClassesOp::Prepend, ClassesOp::Prepend,
[ [
join_pair!( join_pair!("container", "-", self.breakpoint().to_string()),
"container",
"-",
match self.width() {
ContainerWidth::Default => String::new(),
ContainerWidth::From(bp) => bp.to_string(),
ContainerWidth::Fluid => "fluid".to_string(),
ContainerWidth::FluidMax(_) => "fluid".to_string(),
}
),
self.bg_color().to_string(), self.bg_color().to_string(),
self.text_color().to_string(), self.text_color().to_string(),
self.border().to_string(), self.border().to_string(),
@ -100,8 +70,8 @@ impl Component for Container {
if output.is_empty() { if output.is_empty() {
return PrepareMarkup::None; return PrepareMarkup::None;
} }
let style = match self.width() { let style = match self.breakpoint() {
ContainerWidth::FluidMax(w) if w.is_measurable() => { BreakPoint::FluidMax(w) if w.is_measurable() => {
Some(join!("max-width: ", w.to_string(), ";")) Some(join!("max-width: ", w.to_string(), ";"))
} }
_ => None, _ => None,
@ -198,10 +168,24 @@ impl Container {
self self
} }
/// Establece el comportamiento del ancho para el contenedor. /// Establece el *punto de ruptura* del contenedor.
#[builder_fn] #[builder_fn]
pub fn with_width(mut self, width: ContainerWidth) -> Self { pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.container_width = width; self.breakpoint = bp;
self
}
/// Añade un nuevo componente hijo al contenedor.
pub fn add_child(mut self, component: impl Component) -> Self {
self.children
.alter_child(ChildOp::Add(Child::with(component)));
self
}
/// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self self
} }
@ -241,20 +225,6 @@ impl Container {
self self
} }
/// Añade un nuevo componente hijo al contenedor.
#[inline]
pub fn add_child(mut self, component: impl Component) -> Self {
self.children.add(Child::with(component));
self
}
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_child(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// **< Container GETTERS >********************************************************************** // **< Container GETTERS >**********************************************************************
/// Devuelve las clases CSS asociadas al contenedor. /// Devuelve las clases CSS asociadas al contenedor.
@ -267,9 +237,14 @@ impl Container {
&self.container_type &self.container_type
} }
/// Devuelve el comportamiento para el ancho del contenedor. /// Devuelve el *punto de ruptura* actualmente configurado.
pub fn width(&self) -> &ContainerWidth { pub fn breakpoint(&self) -> &BreakPoint {
&self.container_width &self.breakpoint
}
/// Devuelve la lista de hijos (`children`) del contenedor.
pub fn children(&self) -> &Children {
&self.children
} }
/// Devuelve el color de fondo del contenedor. /// Devuelve el color de fondo del contenedor.
@ -291,9 +266,4 @@ impl Container {
pub fn rounded(&self) -> &Rounded { pub fn rounded(&self) -> &Rounded {
&self.rounded &self.rounded
} }
/// Devuelve la lista de componentes (`children`) del contenedor.
pub fn children(&self) -> &Children {
&self.children
}
} }

View file

@ -1,20 +1,5 @@
//! Definiciones para crear menús desplegables [`Dropdown`].
//!
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad: enlaces de
//! navegación, botones de acción, encabezados o divisores visuales.
//!
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
//!
//! Su propósito es ofrecer una base uniforme sobre la que construir menús consistentes, adaptados
//! al contexto de cada aplicación.
mod props;
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
mod component; mod component;
pub use component::Dropdown; pub use component::Dropdown;
mod item; mod item;
pub use item::{Item, ItemKind}; pub use item::Item;

View file

@ -1,48 +1,12 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
/// Componente para crear un **menú desplegable**.
///
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
/// apertura, alineación o política de cierre.
///
/// Sin título para el botón (ver [`with_button_title()`](Self::with_button_title)) se muestra
/// únicamente la lista de elementos sin ningún botón para interactuar.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let dd = Dropdown::new()
/// .with_button_title(L10n::n("Menu"))
/// .with_button_color(ButtonColor::Background(Color::Secondary))
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
/// .with_direction(dropdown::Direction::Dropend)
/// .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/"))
/// .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
/// .add_item(dropdown::Item::divider())
/// .add_item(dropdown::Item::header(L10n::n("User session")))
/// .add_item(dropdown::Item::button(L10n::n("Sign out")));
/// ```
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Dropdown { pub struct Dropdown {
id : AttrId, id : AttrId,
classes : AttrClasses, classes: AttrClasses,
button_title : L10n,
button_size : ButtonSize,
button_color : ButtonColor,
button_split : bool,
button_grouped: bool,
auto_close : dropdown::AutoClose,
direction : dropdown::Direction,
menu_align : dropdown::MenuAlign,
menu_position : dropdown::MenuPosition,
items : Children, items : Children,
} }
@ -55,134 +19,42 @@ impl Component for Dropdown {
self.id.get() self.id.get()
} }
#[rustfmt::skip]
fn setup_before_prepare(&mut self, _cx: &mut Context) { fn setup_before_prepare(&mut self, _cx: &mut Context) {
let g = self.button_grouped(); self.alter_classes(ClassesOp::Prepend, "dropdown");
self.alter_classes(ClassesOp::Prepend, [
if g { "btn-group" } else { "" },
match self.direction() {
dropdown::Direction::Default if g => "",
dropdown::Direction::Default => "dropdown",
dropdown::Direction::Centered => "dropdown-center",
dropdown::Direction::Dropup => "dropup",
dropdown::Direction::DropupCentered => "dropup-center",
dropdown::Direction::Dropend => "dropend",
dropdown::Direction::Dropstart => "dropstart",
}
].join(" "));
} }
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
// Si no hay elementos en el menú, no se prepara.
let items = self.items().render(cx); let items = self.items().render(cx);
if items.is_empty() { if items.is_empty() {
return PrepareMarkup::None; return PrepareMarkup::None;
} }
// Título opcional para el menú desplegable.
let button_title = self.button_title().using(cx);
PrepareMarkup::With(html! { PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] { div id=[self.id()] class=[self.classes().get()] {
@if !button_title.is_empty() {
@let mut btn_classes = AttrClasses::new([
"btn",
&self.button_size().to_string(),
&self.button_color().to_string(),
].join(" "));
@let (offset, reference) = match self.menu_position() {
dropdown::MenuPosition::Default => (None, None),
dropdown::MenuPosition::Offset(x, y) => (Some(format!("{x},{y}")), None),
dropdown::MenuPosition::Parent => (None, Some("parent")),
};
@let auto_close = match self.auto_close {
dropdown::AutoClose::Default => None,
dropdown::AutoClose::ClickableInside => Some("inside"),
dropdown::AutoClose::ClickableOutside => Some("outside"),
dropdown::AutoClose::ManualClose => Some("false"),
};
@let menu_classes = AttrClasses::new("dropdown-menu")
.with_value(ClassesOp::Add, match self.menu_align() {
dropdown::MenuAlign::Start => String::new(),
dropdown::MenuAlign::StartAt(bp) => bp.try_class("dropdown-menu")
.map_or(String::new(), |class| join!(class, "-start")),
dropdown::MenuAlign::StartAndEnd(bp) => bp.try_class("dropdown-menu")
.map_or(
"dropdown-menu-start".into(),
|class| join!("dropdown-menu-start ", class, "-end")
),
dropdown::MenuAlign::End => "dropdown-menu-end".into(),
dropdown::MenuAlign::EndAt(bp) => bp.try_class("dropdown-menu")
.map_or(String::new(), |class| join!(class, "-end")),
dropdown::MenuAlign::EndAndStart(bp) => bp.try_class("dropdown-menu")
.map_or(
"dropdown-menu-end".into(),
|class| join!("dropdown-menu-end ", class, "-start")
),
});
// Renderizado en modo split (dos botones) o simple (un botón).
@if self.button_split() {
// Botón principal (acción/etiqueta).
@let btn = html! {
button button
type="button" type="button"
class=[btn_classes.get()] class="btn btn-secondary dropdown-toggle"
{
(button_title)
}
};
// Botón *toggle* que abre/cierra el menú asociado.
@let btn_toggle = html! {
button
type="button"
class=[btn_classes.alter_value(
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
).get()]
data-bs-toggle="dropdown" data-bs-toggle="dropdown"
data-bs-offset=[offset]
data-bs-reference=[reference]
data-bs-auto-close=[auto_close]
aria-expanded="false" aria-expanded="false"
{ {
span class="visually-hidden" { ("Dropdown button")
(L10n::t("dropdown_toggle", &LOCALES_BOOTSIER).using(cx)) }
ul class="dropdown-menu" {
li {
a class="dropdown-item" href="#" {
("Action")
} }
} }
}; li {
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes). a class="dropdown-item" href="#" {
@match self.direction() { ("Another action")
dropdown::Direction::Dropstart => {
(btn_toggle)
ul class=[menu_classes.get()] { (items) }
(btn)
}
_ => {
(btn)
(btn_toggle)
ul class=[menu_classes.get()] { (items) }
} }
} }
} @else { li {
// Botón único con funcionalidad de *toggle*. a class="dropdown-item" href="#" {
button ("Something else here")
type="button"
class=[btn_classes.alter_value(
ClassesOp::Add, "dropdown-toggle"
).get()]
data-bs-toggle="dropdown"
data-bs-offset=[offset]
data-bs-reference=[reference]
data-bs-auto-close=[auto_close]
aria-expanded="false"
{
(button_title)
} }
ul class=[menu_classes.get()] { (items) }
} }
} @else {
// Sin botón: sólo el listado como menú contextual.
ul class="dropdown-menu" { (items) }
} }
} }
}) })
@ -192,91 +64,23 @@ impl Component for Dropdown {
impl Dropdown { impl Dropdown {
// **< Dropdown BUILDER >*********************************************************************** // **< Dropdown BUILDER >***********************************************************************
/// Establece el identificador único (`id`) del menú desplegable.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id); self.id.alter_value(id);
self self
} }
/// Modifica la lista de clases CSS aplicadas al menú desplegable.
#[builder_fn] #[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self { pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes); self.classes.alter_value(op, classes);
self self
} }
/// Establece el título del botón.
#[builder_fn]
pub fn with_button_title(mut self, title: L10n) -> Self {
self.button_title = title;
self
}
/// Ajusta el tamaño del botón.
#[builder_fn]
pub fn with_button_size(mut self, size: ButtonSize) -> Self {
self.button_size = size;
self
}
/// Define el color/estilo del botón.
#[builder_fn]
pub fn with_button_color(mut self, color: ButtonColor) -> Self {
self.button_color = color;
self
}
/// Activa/desactiva el modo *split* (botón de acción + *toggle*).
#[builder_fn]
pub fn with_button_split(mut self, split: bool) -> Self {
self.button_split = split;
self
}
/// Indica si el botón del menú está integrado en un grupo de botones.
#[builder_fn]
pub fn with_button_grouped(mut self, grouped: bool) -> Self {
self.button_grouped = grouped;
self
}
/// Establece la política de cierre automático del menú desplegable.
#[builder_fn]
pub fn with_auto_close(mut self, auto_close: dropdown::AutoClose) -> Self {
self.auto_close = auto_close;
self
}
/// Establece la dirección de despliegue del menú.
#[builder_fn]
pub fn with_direction(mut self, direction: dropdown::Direction) -> Self {
self.direction = direction;
self
}
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
#[builder_fn]
pub fn with_menu_align(mut self, align: dropdown::MenuAlign) -> Self {
self.menu_align = align;
self
}
/// Configura la posición del menú.
#[builder_fn]
pub fn with_menu_position(mut self, position: dropdown::MenuPosition) -> Self {
self.menu_position = position;
self
}
/// Añade un nuevo elemento hijo al menú.
#[inline]
pub fn add_item(mut self, item: dropdown::Item) -> Self { pub fn add_item(mut self, item: dropdown::Item) -> Self {
self.items.add(Child::with(item)); self.items.add(Child::with(item));
self self
} }
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn] #[builder_fn]
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self { pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
self.items.alter_typed(op); self.items.alter_typed(op);
@ -285,57 +89,10 @@ impl Dropdown {
// **< Dropdown GETTERS >*********************************************************************** // **< Dropdown GETTERS >***********************************************************************
/// Devuelve las clases CSS asociadas al menú desplegable.
pub fn classes(&self) -> &AttrClasses { pub fn classes(&self) -> &AttrClasses {
&self.classes &self.classes
} }
/// Devuelve el título del botón.
pub fn button_title(&self) -> &L10n {
&self.button_title
}
/// Devuelve el tamaño configurado del botón.
pub fn button_size(&self) -> &ButtonSize {
&self.button_size
}
/// Devuelve el color/estilo configurado del botón.
pub fn button_color(&self) -> &ButtonColor {
&self.button_color
}
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
pub fn button_split(&self) -> bool {
self.button_split
}
/// Devuelve si el botón del menú está integrado en un grupo de botones.
pub fn button_grouped(&self) -> bool {
self.button_grouped
}
/// Devuelve la política de cierre automático del menú desplegado.
pub fn auto_close(&self) -> &dropdown::AutoClose {
&self.auto_close
}
/// Devuelve la dirección de despliegue configurada.
pub fn direction(&self) -> &dropdown::Direction {
&self.direction
}
/// Devuelve la configuración de alineación horizontal del menú desplegable.
pub fn menu_align(&self) -> &dropdown::MenuAlign {
&self.menu_align
}
/// Devuelve la posición configurada para el menú desplegable.
pub fn menu_position(&self) -> &dropdown::MenuPosition {
&self.menu_position
}
/// Devuelve la lista de elementos (`children`) del menú.
pub fn items(&self) -> &Children { pub fn items(&self) -> &Children {
&self.items &self.items
} }

View file

@ -1,53 +1,22 @@
use pagetop::prelude::*; use pagetop::prelude::*;
// **< ItemKind >*********************************************************************************** // **< ItemType >***********************************************************************************
/// Tipos de [`dropdown::Item`](crate::theme::dropdown::Item) disponibles en un menú desplegable
/// [`Dropdown`](crate::theme::Dropdown).
///
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
/// con él.
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum ItemKind { pub enum ItemType {
/// Elemento vacío, no produce salida.
#[default] #[default]
Void, Void,
/// Etiqueta sin comportamiento interactivo.
Label(L10n), Label(L10n),
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar Link(L10n, FnPathByContext),
/// inicialmente deshabilitado. LinkBlank(L10n, FnPathByContext),
Link {
label: L10n,
path: FnPathByContext,
blank: bool,
disabled: bool,
},
/// Acción ejecutable en la propia página, sin navegación asociada. Inicialmente puede estar
/// deshabilitado.
Button { label: L10n, disabled: bool },
/// Título o encabezado que separa grupos de opciones.
Header(L10n),
/// Separador visual entre bloques de elementos.
Divider,
} }
// **< Item >*************************************************************************************** // **< Item >***************************************************************************************
/// Representa un **elemento individual** de un menú desplegable
/// [`Dropdown`](crate::theme::Dropdown).
///
/// Cada instancia de [`dropdown::Item`](crate::theme::dropdown::Item) se traduce en un componente
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, según su
/// [`ItemKind`].
///
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
/// manteniendo una interfaz común para renderizar todos los ítems del menú.
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Item { pub struct Item {
id : AttrId, item_type: ItemType,
classes : AttrClasses,
item_kind: ItemKind,
} }
impl Component for Item { impl Component for Item {
@ -55,227 +24,86 @@ impl Component for Item {
Item::default() Item::default()
} }
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.item_kind() { let description: Option<String> = None;
ItemKind::Void => PrepareMarkup::None,
ItemKind::Label(label) => PrepareMarkup::With(html! { // Obtiene la URL actual desde `cx.request`.
li id=[self.id()] class=[self.classes().get()] {
span class="dropdown-item-text" {
(label.using(cx))
}
}
}),
ItemKind::Link {
label,
path,
blank,
disabled,
} => {
let path = path(cx);
let current_path = cx.request().map(|request| request.path()); let current_path = cx.request().map(|request| request.path());
let is_current = !*disabled && current_path.map(|p| p == path).unwrap_or(false);
let mut classes = "dropdown-item".to_string(); match self.item_type() {
if is_current { ItemType::Void => PrepareMarkup::None,
classes.push_str(" active"); ItemType::Label(label) => PrepareMarkup::With(html! {
} li class="dropdown-item" {
if *disabled { span title=[description] {
classes.push_str(" disabled"); //(left_icon)
}
let href = (!disabled).then_some(path);
let target = (!disabled && *blank).then_some("_blank");
let rel = (!disabled && *blank).then_some("noopener noreferrer");
let aria_current = (href.is_some() && is_current).then_some("page");
let aria_disabled = disabled.then_some("true");
let tabindex = disabled.then_some("-1");
PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] {
a
class=(classes)
href=[href]
target=[target]
rel=[rel]
aria-current=[aria_current]
aria-disabled=[aria_disabled]
tabindex=[tabindex]
{
(label.using(cx)) (label.using(cx))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("dropdown-item active", Some("page"))
} else {
("dropdown-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
} }
} }
}) })
} }
ItemType::LinkBlank(label, path) => {
ItemKind::Button { label, disabled } => { let item_path = path(cx);
let mut classes = "dropdown-item".to_owned(); let (class, aria) = if current_path == Some(item_path) {
if *disabled { ("dropdown-item active", Some("page"))
classes.push_str(" disabled"); } else {
} ("dropdown-item", None)
};
let aria_disabled = disabled.then_some("true");
let disabled_attr = disabled.then_some("disabled");
PrepareMarkup::With(html! { PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] { li class=(class) aria-current=[aria] {
button a class="nav-link" href=(item_path) title=[description] target="_blank" {
class=(classes) //(left_icon)
type="button"
aria-disabled=[aria_disabled]
disabled=[disabled_attr]
{
(label.using(cx)) (label.using(cx))
//(right_icon)
} }
} }
}) })
} }
ItemKind::Header(label) => PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] {
h6 class="dropdown-header" {
(label.using(cx))
}
}
}),
ItemKind::Divider => PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
}),
} }
} }
} }
impl Item { impl Item {
/// Crea un elemento de tipo texto, mostrado sin interacción.
pub fn label(label: L10n) -> Self { pub fn label(label: L10n) -> Self {
Item { Item {
item_kind: ItemKind::Label(label), item_type: ItemType::Label(label),
..Default::default() ..Default::default()
} }
} }
/// Crea un enlace para la navegación.
pub fn link(label: L10n, path: FnPathByContext) -> Self { pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item { Item {
item_kind: ItemKind::Link { item_type: ItemType::Link(label, path),
label,
path,
blank: false,
disabled: false,
},
..Default::default() ..Default::default()
} }
} }
/// Crea un enlace deshabilitado que no permite la interacción.
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: false,
disabled: true,
},
..Default::default()
}
}
/// Crea un enlace que se abre en una nueva ventana o pestaña.
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self { pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
Item { Item {
item_kind: ItemKind::Link { item_type: ItemType::LinkBlank(label, path),
label,
path,
blank: true,
disabled: false,
},
..Default::default() ..Default::default()
} }
} }
/// Crea un enlace deshabilitado que se abriría en una nueva ventana. // Item GETTERS.
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: true,
disabled: true,
},
..Default::default()
}
}
/// Crea un botón de acción local, sin navegación asociada. pub fn item_type(&self) -> &ItemType {
pub fn button(label: L10n) -> Self { &self.item_type
Item {
item_kind: ItemKind::Button {
label,
disabled: false,
},
..Default::default()
}
}
/// Crea un botón deshabilitado.
pub fn button_disabled(label: L10n) -> Self {
Item {
item_kind: ItemKind::Button {
label,
disabled: true,
},
..Default::default()
}
}
/// Crea un encabezado para un grupo de elementos dentro del menú.
pub fn header(label: L10n) -> Self {
Item {
item_kind: ItemKind::Header(label),
..Default::default()
}
}
/// Crea un separador visual entre bloques de elementos.
pub fn divider() -> Self {
Item {
item_kind: ItemKind::Divider,
..Default::default()
}
}
// **< Item BUILDER >***************************************************************************
/// Establece el identificador único (`id`) del elemento.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al elemento.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
// **< Item GETTERS >***************************************************************************
/// Devuelve las clases CSS asociadas al elemento.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el tipo de elemento representado por este ítem.
pub fn item_kind(&self) -> &ItemKind {
&self.item_kind
} }
} }

View file

@ -1,86 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
// **< AutoClose >**********************************************************************************
/// Estrategia para el cierre automático de un menú [`Dropdown`].
///
/// Define cuándo se cierra el menú desplegado según la interacción del usuario.
#[derive(AutoDefault)]
pub enum AutoClose {
/// Comportamiento por defecto, se cierra con clics dentro y fuera del menú, o pulsando `Esc`.
#[default]
Default,
/// Sólo se cierra con clics dentro del menú.
ClickableInside,
/// Sólo se cierra con clics fuera del menú.
ClickableOutside,
/// Cierre manual, no se cierra con clics; sólo al pulsar nuevamente el botón del menú
/// (*toggle*), o pulsando `Esc`.
ManualClose,
}
// **< Direction >**********************************************************************************
/// Dirección de despliegue de un menú [`Dropdown`].
///
/// Controla desde qué posición se muestra el menú respecto al botón.
#[derive(AutoDefault)]
pub enum Direction {
/// Comportamiento por defecto (despliega el menú hacia abajo desde la posición inicial,
/// respetando LTR/RTL).
#[default]
Default,
/// Centra horizontalmente el menú respecto al botón.
Centered,
/// Despliega el menú hacia arriba.
Dropup,
/// Despliega el menú hacia arriba y centrado.
DropupCentered,
/// Despliega el menú desde el lateral final, respetando LTR/RTL.
Dropend,
/// Despliega el menú desde el lateral inicial, respetando LTR/RTL.
Dropstart,
}
// **< MenuAlign >**********************************************************************************
/// Alineación horizontal del menú desplegable [`Dropdown`].
///
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]).
#[derive(AutoDefault)]
pub enum MenuAlign {
/// Alineación al inicio (comportamiento por defecto).
#[default]
Start,
/// Alineación al inicio a partir del punto de ruptura indicado.
StartAt(BreakPoint),
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
StartAndEnd(BreakPoint),
/// Alineación al final.
End,
/// Alineación al final a partir del punto de ruptura indicado.
EndAt(BreakPoint),
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
EndAndStart(BreakPoint),
}
// **< MenuPosition >*******************************************************************************
/// Posición relativa del menú desplegable [`Dropdown`].
///
/// Permite indicar un desplazamiento (*offset*) manual o referenciar al elemento padre para el
/// cálculo de la posición.
#[derive(AutoDefault)]
pub enum MenuPosition {
/// Posicionamiento automático por defecto.
#[default]
Default,
/// Desplazamiento manual en píxeles `(x, y)` aplicado al menú. Se admiten valores negativos.
Offset(i8, i8),
/// Posiciona el menú tomando como referencia el botón padre. Especialmente útil cuando
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
Parent,
}

View file

@ -1,7 +1,302 @@
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`]. use pagetop::prelude::*;
mod props; use crate::prelude::*;
pub use props::{Backdrop, BodyScroll, Placement, Visibility}; use crate::LOCALES_BOOTSIER;
mod component; use std::fmt;
pub use component::Offcanvas;
// **< OffcanvasPlacement >*************************************************************************
/// Posición de aparición del panel **deslizante** ([`Offcanvas`]).
///
/// Define desde qué borde de la ventana entra y se ancla el panel.
#[derive(AutoDefault)]
pub enum OffcanvasPlacement {
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
#[default]
Start,
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
End,
/// Desde la parte superior.
Top,
/// Desde la parte inferior.
Bottom,
}
#[rustfmt::skip]
impl fmt::Display for OffcanvasPlacement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasPlacement::Start => f.write_str("offcanvas-start"),
OffcanvasPlacement::End => f.write_str("offcanvas-end"),
OffcanvasPlacement::Top => f.write_str("offcanvas-top"),
OffcanvasPlacement::Bottom => f.write_str("offcanvas-bottom"),
}
}
}
// **< OffcanvasVisibility >************************************************************************
/// Estado inicial del panel ([`Offcanvas`]).
#[derive(AutoDefault)]
pub enum OffcanvasVisibility {
/// El panel **permanece oculto** desde el principio.
#[default]
Default,
/// El panel **se muestra abierto** al cargar.
Show,
}
impl fmt::Display for OffcanvasVisibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasVisibility::Default => Ok(()),
OffcanvasVisibility::Show => f.write_str("show"),
}
}
}
// **< OffcanvasBodyScroll >************************************************************************
/// Controla si la página principal puede **desplazarse** al abrir el panel ([`Offcanvas`]).
#[derive(AutoDefault)]
pub enum OffcanvasBodyScroll {
/// Opción por defecto, la página principal se **bloquea** centrando la interacción en el panel.
#[default]
Disabled,
/// **Permite** el desplazamiento de la página principal.
Enabled,
}
// **< OffcanvasBackdrop >**************************************************************************
/// Comportamiento de la **capa de fondo** (*backdrop*) del panel ([`Offcanvas`]) al desplegarse.
#[derive(AutoDefault)]
pub enum OffcanvasBackdrop {
/// **Sin capa** de fondo; la página principal permanece visible e interactiva.
Disabled,
/// Opción por defecto, se **oscurece** el fondo; un clic fuera del panel suele cerrarlo.
#[default]
Enabled,
/// Se muestra capa de fondo pero **no** se cierra al pulsar fuera (útil cuando se requiere
/// completar una acción antes de salir).
Static,
}
// **< Offcanvas >**********************************************************************************
/// Panel lateral **deslizante** para contenido complementario.
///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
/// características principales:
///
/// - **Entrada configurable desde un borde** de la ventana.
/// - **Encabezado con título** y **botón de cierre** integrado.
/// - **Accesibilidad**: asocia título y controles a un identificador único y expone atributos
/// adecuados para lectores de pantalla y navegación por teclado.
/// - **Opcionalmente** bloquea el desplazamiento del documento y/o muestra una capa de fondo para
/// centrar la atención del usuario.
/// - **Responsive**: puede cambiar su comportamiento según el punto de ruptura indicado.
/// - **No se renderiza** si no tiene contenido.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Offcanvas {
id : AttrId,
classes : AttrClasses,
title : L10n,
breakpoint: BreakPoint,
placement : OffcanvasPlacement,
visibility: OffcanvasVisibility,
scrolling : OffcanvasBodyScroll,
backdrop : OffcanvasBackdrop,
children : Children,
}
impl Component for Offcanvas {
fn new() -> Self {
Offcanvas::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
self.breakpoint().to_class("offcanvas"),
self.placement().to_string(),
self.visibility().to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let body = self.children().render(cx);
if body.is_empty() {
return PrepareMarkup::None;
}
let id = cx.required_id::<Self>(self.id());
let id_label = join!(id, "-label");
let id_target = join!("#", id);
let body_scroll = match self.body_scroll() {
OffcanvasBodyScroll::Disabled => None,
OffcanvasBodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
OffcanvasBackdrop::Disabled => Some("false"),
OffcanvasBackdrop::Enabled => None,
OffcanvasBackdrop::Static => Some("static"),
};
let title = self.title().using(cx);
PrepareMarkup::With(html! {
div
id=(id)
class=[self.classes().get()]
tabindex="-1"
data-bs-scroll=[body_scroll]
data-bs-backdrop=[backdrop]
aria-labelledby=(id_label)
{
div class="offcanvas-header" {
@if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) }
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("close", &LOCALES_BOOTSIER).lookup(cx)]
{}
}
div class="offcanvas-body" {
(body)
}
}
})
}
}
impl Offcanvas {
// **< Offcanvas BUILDER >**********************************************************************
/// Establece el identificador único (`id`) del panel.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al panel.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Establece el **título** del encabezado.
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title = title;
self
}
/// Configura el **punto de ruptura** para activar el comportamiento responsive del panel.
#[builder_fn]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
/// Indica la **posición** desde la que entra el panel.
#[builder_fn]
pub fn with_placement(mut self, placement: OffcanvasPlacement) -> Self {
self.placement = placement;
self
}
/// Fija el **estado inicial** del panel (oculto o visible al cargar).
#[builder_fn]
pub fn with_visibility(mut self, visibility: OffcanvasVisibility) -> Self {
self.visibility = visibility;
self
}
/// Permite o bloquea el **desplazamiento** de la página principal mientras el panel está
/// abierto.
#[builder_fn]
pub fn with_body_scroll(mut self, scrolling: OffcanvasBodyScroll) -> Self {
self.scrolling = scrolling;
self
}
/// Ajusta la **capa de fondo** del panel para definir su comportamiento al interactuar fuera.
#[builder_fn]
pub fn with_backdrop(mut self, backdrop: OffcanvasBackdrop) -> Self {
self.backdrop = backdrop;
self
}
/// Añade un nuevo componente hijo al panel.
pub fn add_child(mut self, child: impl Component) -> Self {
self.children.add(Child::with(child));
self
}
/// Modifica la lista de hijos (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// **< Offcanvas GETTERS >**********************************************************************
/// Devuelve las clases CSS asociadas al panel.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el título del panel como [`L10n`].
pub fn title(&self) -> &L10n {
&self.title
}
/// Devuelve el punto de ruptura configurado.
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
/// Devuelve la posición del panel.
pub fn placement(&self) -> &OffcanvasPlacement {
&self.placement
}
/// Devuelve el estado inicial del panel.
pub fn visibility(&self) -> &OffcanvasVisibility {
&self.visibility
}
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
pub fn body_scroll(&self) -> &OffcanvasBodyScroll {
&self.scrolling
}
/// Devuelve la configuración de la capa de fondo.
pub fn backdrop(&self) -> &OffcanvasBackdrop {
&self.backdrop
}
/// Devuelve la lista de hijos (`children`) del panel.
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -1,261 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales.
///
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
/// características principales:
///
/// - Puede mostrar una capa de fondo para centrar la atención del usuario en el panel
/// ([`with_backdrop()`](Self::with_backdrop)); o puede bloquear el desplazamiento del documento
/// principal ([`with_body_scroll()`](Self::with_body_scroll)).
/// - Se puede configurar el borde de la ventana desde el que se desliza el panel
/// ([`with_placement()`](Self::with_placement)).
/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado.
/// - Puede cambiar su comportamiento a partir de un punto de ruptura
/// ([`with_breakpoint()`](Self::with_breakpoint)).
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
/// adecuados para lectores de pantalla y navegación por teclado.
/// - **No se renderiza** si no tiene contenido.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let panel = Offcanvas::new()
/// .with_id("offcanvas_example")
/// .with_title(L10n::n("Offcanvas title"))
/// .with_placement(offcanvas::Placement::End)
/// .with_backdrop(offcanvas::Backdrop::Enabled)
/// .with_body_scroll(offcanvas::BodyScroll::Enabled)
/// .with_visibility(offcanvas::Visibility::Default)
/// .add_child(Dropdown::new()
/// .with_button_title(L10n::n("Menu"))
/// .add_item(dropdown::Item::label(L10n::n("Label")))
/// .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
/// .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
/// );
/// ```
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Offcanvas {
id : AttrId,
classes : AttrClasses,
title : L10n,
breakpoint: BreakPoint,
backdrop : offcanvas::Backdrop,
scrolling : offcanvas::BodyScroll,
placement : offcanvas::Placement,
visibility: offcanvas::Visibility,
children : Children,
}
impl Component for Offcanvas {
fn new() -> Self {
Offcanvas::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
#[rustfmt::skip]
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
self.breakpoint().to_class("offcanvas"),
match self.placement() {
offcanvas::Placement::Start => "offcanvas-start",
offcanvas::Placement::End => "offcanvas-end",
offcanvas::Placement::Top => "offcanvas-top",
offcanvas::Placement::Bottom => "offcanvas-bottom",
}.to_string(),
match self.visibility() {
offcanvas::Visibility::Default => "",
offcanvas::Visibility::Show => "show",
}.to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let body = self.children().render(cx);
if body.is_empty() {
return PrepareMarkup::None;
}
let id = cx.required_id::<Self>(self.id());
let id_label = join!(id, "-label");
let id_target = join!("#", id);
let body_scroll = match self.body_scroll() {
offcanvas::BodyScroll::Disabled => None,
offcanvas::BodyScroll::Enabled => Some("true"),
};
let backdrop = match self.backdrop() {
offcanvas::Backdrop::Disabled => Some("false"),
offcanvas::Backdrop::Enabled => None,
offcanvas::Backdrop::Static => Some("static"),
};
let title = self.title().using(cx);
PrepareMarkup::With(html! {
div
id=(id)
class=[self.classes().get()]
tabindex="-1"
data-bs-scroll=[body_scroll]
data-bs-backdrop=[backdrop]
aria-labelledby=(id_label)
{
div class="offcanvas-header" {
@if !title.is_empty() {
h5 class="offcanvas-title" id=(id_label) { (title) }
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
{}
}
div class="offcanvas-body" {
(body)
}
}
})
}
}
impl Offcanvas {
// **< Offcanvas BUILDER >**********************************************************************
/// Establece el identificador único (`id`) del panel.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Modifica la lista de clases CSS aplicadas al panel.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Establece el título del encabezado.
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title = title;
self
}
/// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel.
///
/// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante
/// ([`Offcanvas`]).
/// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página.
///
/// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible
/// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea
/// *offcanvas* siempre.
#[builder_fn]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
/// panel.
#[builder_fn]
pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self {
self.backdrop = backdrop;
self
}
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
#[builder_fn]
pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self {
self.scrolling = scrolling;
self
}
/// Indica desde qué borde de la ventana entra y se ancla el panel.
#[builder_fn]
pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self {
self.placement = placement;
self
}
/// Fija el estado inicial del panel (oculto o visible al cargar).
#[builder_fn]
pub fn with_visibility(mut self, visibility: offcanvas::Visibility) -> Self {
self.visibility = visibility;
self
}
/// Añade un nuevo componente hijo al panel.
#[inline]
pub fn add_child(mut self, child: impl Component) -> Self {
self.children.add(Child::with(child));
self
}
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
#[builder_fn]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// **< Offcanvas GETTERS >**********************************************************************
/// Devuelve las clases CSS asociadas al panel.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el título del panel.
pub fn title(&self) -> &L10n {
&self.title
}
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
/// Devuelve el comportamiento configurado para la capa de fondo.
pub fn backdrop(&self) -> &offcanvas::Backdrop {
&self.backdrop
}
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
pub fn body_scroll(&self) -> &offcanvas::BodyScroll {
&self.scrolling
}
/// Devuelve la posición de inicio del panel.
pub fn placement(&self) -> &offcanvas::Placement {
&self.placement
}
/// Devuelve el estado inicial del panel.
pub fn visibility(&self) -> &offcanvas::Visibility {
&self.visibility
}
/// Devuelve la lista de componentes (`children`) del panel.
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -1,60 +0,0 @@
use pagetop::prelude::*;
// **< Backdrop >***********************************************************************************
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
#[derive(AutoDefault)]
pub enum Backdrop {
/// Sin capa de fondo, la página principal permanece visible e interactiva.
Disabled,
/// Opción por defecto, se oscurece el fondo; un clic fuera del panel suele cerrarlo.
#[default]
Enabled,
/// Muestra la capa de fondo pero no se cierra al hacer clic fuera del panel. Útil si se
/// requiere completar una acción antes de salir.
Static,
}
// **< BodyScroll >*********************************************************************************
/// Controla si la página principal puede desplazarse al abrir un panel
/// [`Offcanvas`](crate::theme::Offcanvas).
#[derive(AutoDefault)]
pub enum BodyScroll {
/// Opción por defecto, la página principal se bloquea centrando la interacción en el panel.
#[default]
Disabled,
/// Permite el desplazamiento de la página principal.
Enabled,
}
// **< Placement >**********************************************************************************
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
///
/// Define desde qué borde de la ventana entra y se ancla el panel.
#[derive(AutoDefault)]
pub enum Placement {
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
#[default]
Start,
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
End,
/// Desde la parte superior.
Top,
/// Desde la parte inferior.
Bottom,
}
// **< Visibility >*********************************************************************************
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
#[derive(AutoDefault)]
pub enum Visibility {
/// El panel permanece oculto desde el principio.
#[default]
Default,
/// El panel se muestra abierto al cargar.
Show,
}

View file

@ -277,7 +277,8 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
.collect(); .collect();
// Documentación del método alter_...(). // Documentación del método alter_...().
let doc = format!("Equivale a [`Self::{with_name_str}()`], pero fuera del patrón *builder*."); let alter_doc =
format!("Equivalente a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
// Genera el código final. // Genera el código final.
let expanded = match body_opt { let expanded = match body_opt {
@ -287,7 +288,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
fn #with_name #generics (self, #(#args),*) -> Self #where_clause; fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
#(#non_doc_or_inline_attrs)* #(#non_doc_or_inline_attrs)*
#[doc = #doc] #[doc = #alter_doc]
fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause; fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
} }
} }
@ -321,7 +322,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
#with_fn #with_fn
#(#non_doc_or_inline_attrs)* #(#non_doc_or_inline_attrs)*
#[doc = #doc] #[doc = #alter_doc]
#vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause { #vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
#body #body
} }

View file

@ -91,7 +91,7 @@ impl Block {
&self.classes &self.classes
} }
/// Devuelve el título del bloque. /// Devuelve el título del bloque como [`L10n`].
pub fn title(&self) -> &L10n { pub fn title(&self) -> &L10n {
&self.title &self.title
} }

View file

@ -140,7 +140,20 @@ impl Component for Intro {
} }
aside class="intro-header__image" aria-hidden="true" { aside class="intro-header__image" aria-hidden="true" {
div class="intro-header__monster" { div class="intro-header__monster" {
(PageTopSvg::Color.render(cx)) picture {
source
type="image/avif"
src="/img/monster-pagetop_250.avif"
srcset="/img/monster-pagetop_500.avif 1.5x";
source
type="image/webp"
src="/img/monster-pagetop_250.webp"
srcset="/img/monster-pagetop_500.webp 1.5x";
img
src="/img/monster-pagetop_250.png"
srcset="/img/monster-pagetop_500.png 1.5x"
alt="Monster PageTop";
}
} }
} }
} }
@ -189,7 +202,18 @@ impl Component for Intro {
div class="intro-footer" { div class="intro-footer" {
section class="intro-footer__body" { section class="intro-footer__body" {
div class="intro-footer__logo" { div class="intro-footer__logo" {
(PageTopSvg::LineLight.render(cx)) svg
viewBox="0 0 1614 1614"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label=[L10n::l("pagetop_logo").lookup(cx)]
preserveAspectRatio="xMidYMid slice"
focusable="false"
{
path fill="rgb(255,255,255)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
path fill="rgb(255,255,255)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
path fill="rgb(255,255,255)" 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" {}
}
} }
div class="intro-footer__links" { div class="intro-footer__links" {
a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") } a href="https://crates.io/crates/pagetop" target="_blank" rel="noopener noreferrer" { ("Crates.io") }
@ -213,7 +237,7 @@ impl Intro {
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let intro = Intro::default().with_title(L10n::n("Intro title")); /// let intro = Intro::default().with_title(L10n::n("Título de entrada"));
/// ``` /// ```
#[builder_fn] #[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self { pub fn with_title(mut self, title: L10n) -> Self {
@ -227,7 +251,7 @@ impl Intro {
/// ///
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// let intro = Intro::default().with_slogan(L10n::n("A short slogan")); /// let intro = Intro::default().with_slogan(L10n::n("Un eslogan para la entrada"));
/// ``` /// ```
#[builder_fn] #[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self { pub fn with_slogan(mut self, slogan: L10n) -> Self {
@ -246,7 +270,7 @@ impl Intro {
/// ```rust /// ```rust
/// # use pagetop::prelude::*; /// # use pagetop::prelude::*;
/// // Define un botón con texto y una URL fija. /// // Define un botón con texto y una URL fija.
/// let intro = Intro::default().with_button(Some((L10n::n("Learn more"), |_| "/start"))); /// let intro = Intro::default().with_button(Some((L10n::n("Pulsa este botón"), |_| "/start")));
/// // Descarta el botón de la intro. /// // Descarta el botón de la intro.
/// let intro_no_button = Intro::default().with_button(None); /// let intro_no_button = Intro::default().with_button(None);
/// ``` /// ```

View file

@ -21,7 +21,7 @@ pub type ExtensionRef = &'static dyn Extension;
/// ///
/// impl Extension for Blog { /// impl Extension for Blog {
/// fn name(&self) -> L10n { L10n::n("Blog") } /// fn name(&self) -> L10n { L10n::n("Blog") }
/// fn description(&self) -> L10n { L10n::n("Blog system") } /// fn description(&self) -> L10n { L10n::n("Sistema de blogs") }
/// } /// }
/// ``` /// ```
pub trait Extension: AnyInfo + Send + Sync { pub trait Extension: AnyInfo + Send + Sync {

View file

@ -11,9 +11,6 @@ pub use assets::javascript::JavaScript;
pub use assets::stylesheet::{StyleSheet, TargetMedia}; pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub use assets::{Asset, Assets}; pub use assets::{Asset, Assets};
mod logo;
pub use logo::PageTopSvg;
// **< HTML DOCUMENT CONTEXT >********************************************************************** // **< HTML DOCUMENT CONTEXT >**********************************************************************
/// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Context`] en su lugar. /// **Obsoleto desde la versión 0.5.0**: usar [`core::component::Context`] en su lugar.

View file

@ -1,93 +0,0 @@
use crate::core::component::Context;
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::AutoDefault;
/// Representación SVG del **logotipo de PageTop** para incrustar en HTML.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// fn render_logo(cx: &mut Context) -> PrepareMarkup {
/// PrepareMarkup::With(html! {
/// div class="logo_color" {
/// (PageTopSvg::Color.render(cx))
/// }
/// div class="line_dark" {
/// (PageTopSvg::LineDark.render(cx))
/// }
/// div class="line_light" {
/// (PageTopSvg::LineLight.render(cx))
/// }
/// div class="line_red" {
/// (PageTopSvg::LineRGB(255, 0, 0).render(cx))
/// }
/// })
/// };
/// ```
#[derive(AutoDefault)]
pub enum PageTopSvg {
/// Versión por defecto con el logotipo a color.
#[default]
Color,
/// Versión monocroma (línea) oscura, ideal sobre fondos claros.
LineDark,
/// Versión monocroma (línea) clara, ideal sobre fondos oscuros.
LineLight,
/// Versión monocroma configurable por RGB.
LineRGB(u8, u8, u8),
}
impl PageTopSvg {
/// Renderiza el SVG del logotipo según la variante elegida.
pub fn render(&self, cx: &Context) -> Markup {
let path_fills = match self {
Self::Color => self.logo_color(),
Self::LineDark => self.logo_line(10, 11, 9),
Self::LineLight => self.logo_line(255, 255, 255),
Self::LineRGB(r, g, b) => self.logo_line(*r, *g, *b),
};
html! {
svg
viewBox="0 0 1614 1614"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label=[L10n::l("pagetop_logo").lookup(cx)]
preserveAspectRatio="xMidYMid slice"
focusable="false"
{
(path_fills)
}
}
}
// **< PageTopSvg HELPERS >*********************************************************************
fn logo_color(&self) -> Markup {
html! {
path fill="rgb(255,184,75)" d="M 633,61 L 633,61 C 579,61 527,75 480,102 433,129 395,167 368,214 341,261 327,313 327,367 L 327,1244 327,1245 C 327,1299 341,1351 368,1398 395,1445 433,1483 480,1510 527,1537 579,1551 633,1551 L 982,1550 982,1551 C 1036,1551 1088,1537 1135,1510 1182,1483 1220,1445 1247,1398 1274,1351 1288,1299 1288,1245 L 1288,367 1288,367 1288,367 C 1288,313 1274,261 1247,214 1220,167 1182,129 1135,102 1088,75 1036,61 982,61 L 633,61 Z" {}
path fill="rgb(158,96,0)" d="M 1389,573 L 1328,573 1328,460 1449,460 1449,573 1389,573 Z M 1491,627 L 1430,627 1430,404 1551,404 1551,627 1491,627 Z M 222,573 L 161,573 161,460 282,460 282,573 222,573 Z M 120,627 L 59,627 59,404 180,404 180,627 120,627 Z" {}
path fill="rgb(150,0,184)" d="M 678,1040 L 678,1040 C 645,1040 612,1049 583,1065 554,1082 530,1106 513,1135 497,1164 488,1197 488,1230 L 488,1230 488,1230 C 488,1263 497,1296 513,1325 530,1354 554,1378 583,1395 612,1411 645,1420 678,1420 L 940,1420 940,1420 C 973,1420 1006,1411 1035,1395 1064,1378 1088,1354 1105,1325 1121,1296 1130,1263 1130,1230 L 1130,1230 1130,1230 1130,1230 C 1130,1197 1121,1164 1105,1135 1088,1106 1064,1082 1035,1065 1006,1049 973,1040 940,1040 L 678,1040 Z M 488,1040 L 488,1040 488,1040 488,1040 488,1040 488,1040 488,1238 488,1238 488,1238 488,1238 488,1238 1130,1238 1130,1238 1130,1238 1130,1238 1130,1238 1130,1238 1130,1040 1130,1040 1130,1040 1130,1040 1130,1040 488,1040 Z" {}
path fill="rgb(255,255,255)" d="M 518,1128 L 488,1128 488,1066 547,1066 547,1128 518,1128 Z M 966,1128 L 908,1128 908,1066 1024,1066 1024,1128 966,1128 Z" {}
path fill="rgb(221,255,149)" d="M 898,71 C 940,48 987,36 1035,36 1086,36 1137,50 1181,77 1226,104 1263,142 1289,189 1314,236 1328,288 1328,342 1328,396 1314,448 1289,495 1281,509 1272,522 1262,535 L 898,71 Z M 1337,429 C 1307,460 1272,480 1233,488 1192,496 1148,490 1107,470 1066,451 1029,418 999,375 969,332 948,281 938,227 927,173 928,117 940,66 943,51 948,36 953,22 L 1337,429 Z" {}
path fill="rgb(146,128,99)" d="M 703,99 C 717,121 722,149 719,178 715,208 702,238 682,267 661,295 634,320 602,340 571,360 536,373 501,379 467,385 434,383 405,373 377,363 355,346 341,323 327,301 322,273 325,244 329,214 342,184 362,155 383,127 410,102 442,82 473,62 508,49 543,43 577,37 610,39 639,49 667,59 689,76 703,99 L 703,99 Z" {}
path fill="rgb(146,128,99)" d="M 672,550 C 683,568 685,590 678,615 671,640 655,668 632,694 609,720 580,745 547,765 514,785 479,801 446,810 412,819 381,821 355,816 329,811 310,799 299,782 288,765 286,742 293,717 301,692 316,665 339,638 362,612 391,588 424,567 457,547 492,531 526,523 559,514 591,511 616,516 642,521 661,533 672,550 L 672,550 Z" {}
path fill="rgb(146,128,99)" d="M 455,160 L 456,160 C 430,160 404,167 381,180 359,193 340,212 327,234 314,257 307,283 307,309 L 307,580 307,580 C 307,606 314,632 327,655 340,677 359,696 381,709 404,722 430,729 456,729 L 529,729 529,729 C 555,729 581,722 604,709 626,696 645,677 658,655 671,632 678,606 678,580 L 677,308 678,309 678,309 C 678,283 671,257 658,234 645,212 626,193 604,180 581,167 555,160 529,160 L 455,160 Z" {}
path fill="rgb(240,0,0)" d="M 698,1332 L 699,1332 C 696,1332 694,1333 691,1334 689,1335 687,1337 686,1339 685,1342 684,1344 684,1347 L 684,1405 684,1405 C 684,1408 685,1410 686,1413 687,1415 689,1417 691,1418 694,1419 696,1420 699,1420 L 914,1419 914,1420 C 917,1420 919,1419 922,1418 924,1417 926,1415 927,1413 928,1410 929,1408 929,1405 L 929,1346 929,1347 929,1347 C 929,1344 928,1342 927,1339 926,1337 924,1335 922,1334 919,1333 917,1332 914,1332 L 698,1332 Z M 643,780 C 643,797 638,814 630,830 621,845 608,858 593,867 577,875 560,880 543,880 525,880 508,875 492,867 477,858 464,845 455,830 447,814 442,797 442,780 442,762 447,745 455,729 464,714 477,701 492,692 508,684 525,679 542,679 560,679 577,684 593,692 608,701 621,714 630,729 638,745 643,762 643,779 L 643,780 Z M 1171,780 C 1171,797 1166,814 1158,830 1149,845 1136,858 1121,867 1105,875 1088,880 1071,880 1053,880 1036,875 1020,867 1005,858 992,845 983,830 975,814 970,797 970,780 970,762 975,745 983,729 992,714 1005,701 1020,692 1036,684 1053,679 1071,679 1088,679 1105,684 1121,692 1136,701 1149,714 1158,729 1166,745 1171,762 1171,779 L 1171,780 Z" {}
path fill="rgb(10,11,9)" d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
path fill="rgb(10,11,9)" d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
path fill="rgb(10,11,9)" 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_line(&self, r: u8, g: u8, b: u8) -> Markup {
let logo_rgb = format!("rgb({r},{g},{b})");
html! {
path fill=(logo_rgb) d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
path fill=(logo_rgb) d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
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" {}
}
}
}

View file

@ -33,8 +33,8 @@ pub use crate::trace;
// alias obsoletos se volverá a declarar como `pub use crate::html::*;`. // alias obsoletos se volverá a declarar como `pub use crate::html::*;`.
pub use crate::html::{ pub use crate::html::{
display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue, display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue,
ClassesOp, Escaper, Favicon, JavaScript, Markup, PageTopSvg, PreEscaped, PrepareMarkup, ClassesOp, Escaper, Favicon, JavaScript, Markup, PreEscaped, PrepareMarkup, StyleSheet,
StyleSheet, TargetMedia, UnitValue, DOCTYPE, TargetMedia, UnitValue, DOCTYPE,
}; };
pub use crate::locale::*; pub use crate::locale::*;

View file

@ -103,10 +103,9 @@
width: 100%; width: 100%;
} }
.intro-header__monster { .intro-header__monster {
margin: 2rem; margin-right: 12rem;
margin-top: 1rem;
flex-shrink: 1; flex-shrink: 1;
width: 280px;
height: 280px;
} }
@media (min-width: 64rem) { @media (min-width: 64rem) {
.intro-header { .intro-header {
@ -119,9 +118,6 @@
.intro-header__image { .intro-header__image {
justify-content: flex-end; justify-content: flex-end;
} }
.intro-header__monster {
margin-right: 12rem;
}
} }
/* /*

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB