[bootsier] Añade más componentes y repasa código

- Se incorpora nuevo componente Dropdown.
- Se crea un componente Navbar con soporte para marca, elementos de
navegación.
- Se implementa el componente Offcanvas con opciones de posición,
visibilidad y fondo personalizables.
- Mejora el manejo de imágenes con un nuevo componente de Image.
- Se reorganizan los componentes del tema para una mejor estructura y
usabilidad.
This commit is contained in:
Manuel Cillero 2025-10-19 21:57:15 +02:00
parent cf40af13bb
commit 82837c622e
29 changed files with 2608 additions and 9 deletions

View file

@ -0,0 +1,104 @@
use pagetop::prelude::*;
use crate::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Brand {
id : AttrId,
#[default(_code = "global::SETTINGS.app.name.to_owned()")]
app_name : String,
slogan : AttrL10n,
logo : Typed<Image>,
#[default(_code = "|_| \"/\"")]
home : FnPathByContext,
}
impl Component for Brand {
fn new() -> Self {
Brand::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let logo = self.logo().render(cx);
let home = self.home()(cx);
let title = &L10n::l("site_home").lookup(cx);
PrepareMarkup::With(html! {
div id=[self.id()] class="branding__container" {
div class="branding__content" {
@if !logo.is_empty() {
a class="branding__logo" href=(home) title=[title] rel="home" {
(logo)
}
}
div class="branding__text" {
a class="branding__name" href=(home) title=[title] rel="home" {
(self.app_name())
}
@if let Some(slogan) = self.slogan().lookup(cx) {
div class="branding__slogan" {
(slogan)
}
}
}
}
}
})
}
}
impl Brand {
// Brand BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = app_name.into();
self
}
#[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan.alter_value(slogan);
self
}
#[builder_fn]
pub fn with_logo(mut self, logo: Option<Image>) -> Self {
self.logo.alter_component(logo);
self
}
#[builder_fn]
pub fn with_home(mut self, home: FnPathByContext) -> Self {
self.home = home;
self
}
// Brand GETTERS.
pub fn app_name(&self) -> &String {
&self.app_name
}
pub fn slogan(&self) -> &AttrL10n {
&self.slogan
}
pub fn logo(&self) -> &Typed<Image> {
&self.logo
}
pub fn home(&self) -> &FnPathByContext {
&self.home
}
}

View file

@ -0,0 +1,73 @@
use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use std::fmt;
#[derive(AutoDefault, PartialEq)]
pub(crate) enum Toggle {
#[default]
Collapse,
Offcanvas,
}
#[rustfmt::skip]
impl fmt::Display for Toggle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Toggle::Collapse => write!(f, "collapse"),
Toggle::Offcanvas => write!(f, "offcanvas"),
}
}
}
#[derive(AutoDefault)]
pub struct ButtonToggler;
impl Component for ButtonToggler {
fn new() -> Self {
ButtonToggler::default()
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
button
type="button"
class="navbar-toggler"
{
span class="navbar-toggler-icon" {}
}
})
}
}
impl ButtonToggler {
// ButtonToggler PRIVATE RENDER.
pub(crate) fn render(
&self,
cx: &mut Context,
id_content: String,
data_bs_toggle: Toggle,
) -> Markup {
let id_content_target = join!("#", id_content);
let aria_expanded = if data_bs_toggle == Toggle::Collapse {
Some("false")
} else {
None
};
html! {
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{
span class="navbar-toggler-icon" {}
}
}
}
}

View file

@ -0,0 +1,233 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
#[derive(AutoDefault)]
pub enum NavbarToggler {
#[default]
Enabled,
Disabled,
}
#[derive(AutoDefault)]
pub enum NavbarType {
#[default]
None,
Nav(Typed<navbar::Nav>),
Offcanvas(Typed<Offcanvas>),
Text(L10n),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Navbar {
id : AttrId,
classes : AttrClasses,
expand : BreakPoint,
toggler : NavbarToggler,
navbar_type: NavbarType,
contents : Children,
brand : Typed<navbar::Brand>,
}
impl Component for Navbar {
fn new() -> Self {
Navbar::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
"navbar".to_string(),
self.expand().try_class("navbar-expand").unwrap_or_default(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let id = cx.required_id::<Self>(self.id());
let navbar_type = match self.navbar_type() {
NavbarType::None => return PrepareMarkup::None,
NavbarType::Nav(nav) => {
let id_content = join!(id, "-content");
match self.toggler() {
NavbarToggler::Enabled => self.toggler_wrapper(
TOGGLE_COLLAPSE,
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
id_content,
self.brand().render(cx),
nav.render(cx),
),
NavbarToggler::Disabled => nav.render(cx),
}
}
NavbarType::Offcanvas(oc) => {
let id_content = oc.id().unwrap_or_default();
self.toggler_wrapper(
TOGGLE_OFFCANVAS,
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
id_content,
self.brand().render(cx),
oc.render(cx),
)
}
NavbarType::Text(text) => html! {
span class="navbar-text" {
(text.using(cx))
}
},
};
self.nav_wrapper(id, self.brand().render(cx), navbar_type)
}
}
impl Navbar {
pub fn with_nav(nav: navbar::Nav) -> Self {
Navbar::default().with_navbar_type(NavbarType::Nav(Typed::with(nav)))
}
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
Navbar::default().with_navbar_type(NavbarType::Offcanvas(Typed::with(offcanvas)))
}
// Navbar BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[builder_fn]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp;
self
}
#[builder_fn]
pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
self.toggler = toggler;
self
}
#[builder_fn]
pub fn with_navbar_type(mut self, navbar_type: NavbarType) -> Self {
self.navbar_type = navbar_type;
self
}
pub fn with_content(mut self, content: navbar::Content) -> Self {
self.contents.add(Child::with(content));
self
}
#[builder_fn]
pub fn with_contents(mut self, op: TypedOp<navbar::Content>) -> Self {
self.contents.alter_typed(op);
self
}
#[builder_fn]
pub fn with_brand(mut self, brand: Option<navbar::Brand>) -> Self {
self.brand.alter_component(brand);
self
}
// Navbar GETTERS.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn expand(&self) -> &BreakPoint {
&self.expand
}
pub fn toggler(&self) -> &NavbarToggler {
&self.toggler
}
pub fn navbar_type(&self) -> &NavbarType {
&self.navbar_type
}
pub fn contents(&self) -> &Children {
&self.contents
}
pub fn brand(&self) -> &Typed<navbar::Brand> {
&self.brand
}
// Navbar HELPERS.
fn nav_wrapper(&self, id: String, brand: Markup, content: Markup) -> PrepareMarkup {
if content.is_empty() {
PrepareMarkup::None
} else {
PrepareMarkup::With(html! {
(brand)
nav id=(id) class=[self.classes().get()] {
div class="container-fluid" {
(content)
}
}
})
}
}
fn toggler_wrapper(
&self,
data_bs_toggle: &str,
aria_label: Option<String>,
id_content: String,
brand: Markup,
content: Markup,
) -> Markup {
if content.is_empty() {
html! {}
} else {
let id_content_target = join!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false")
} else {
None
};
html! {
(brand)
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[aria_label]
{
span class="navbar-toggler-icon" {}
}
div id=(id_content) class="collapse navbar-collapse" {
(content)
}
}
}
}
}

View file

@ -0,0 +1,69 @@
use pagetop::prelude::*;
use crate::theme::navbar;
#[derive(AutoDefault)]
pub enum ContentType {
#[default]
None,
Brand(Typed<navbar::Brand>),
Nav(Typed<navbar::Nav>),
Text(L10n),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Content {
content: ContentType,
}
impl Component for Content {
fn new() -> Self {
Content::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.content() {
ContentType::None => PrepareMarkup::None,
ContentType::Brand(brand) => PrepareMarkup::With(html! {
(brand.render(cx))
}),
ContentType::Nav(nav) => PrepareMarkup::With(html! {
(nav.render(cx))
}),
ContentType::Text(text) => PrepareMarkup::With(html! {
span class="navbar-text" {
(text.using(cx))
}
}),
}
}
}
impl Content {
pub fn brand(content: navbar::Brand) -> Self {
Content {
content: ContentType::Brand(Typed::with(content)),
}
}
pub fn nav(content: navbar::Nav) -> Self {
Content {
content: ContentType::Nav(Typed::with(content)),
}
}
pub fn text(content: L10n) -> Self {
Content {
content: ContentType::Text(content),
}
}
// Content GETTERS.
pub fn content(&self) -> &ContentType {
&self.content
}
}

View file

@ -0,0 +1,113 @@
use pagetop::prelude::*;
use crate::theme::Dropdown;
type Label = L10n;
#[derive(AutoDefault)]
pub enum ItemType {
#[default]
Void,
Label(Label),
Link(Label, FnPathByContext),
LinkBlank(Label, FnPathByContext),
Dropdown(Typed<Dropdown>),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_type: ItemType,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description: Option<String> = None;
// Obtiene la URL actual desde `cx.request`.
let current_path = cx.request().map(|request| request.path());
match self.item_type() {
ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! {
li class="nav-item" {
span title=[description] {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("nav-item active", Some("page"))
} else {
("nav-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) => {
let item_path = path(cx);
let (class, aria) = if current_path == Some(item_path) {
("nav-item active", Some("page"))
} else {
("nav-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] target="_blank" {
//(left_icon)
(label.using(cx))
//(right_icon)
}
}
})
}
ItemType::Dropdown(menu) => PrepareMarkup::With(html! { (menu.render(cx)) }),
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_type: ItemType::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
Item {
item_type: ItemType::LinkBlank(label, path),
..Default::default()
}
}
// Item GETTERS.
pub fn item_type(&self) -> &ItemType {
&self.item_type
}
}

View file

@ -0,0 +1,75 @@
use pagetop::prelude::*;
use crate::theme::navbar;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Nav {
id : AttrId,
classes: AttrClasses,
items : Children,
}
impl Component for Nav {
fn new() -> Self {
Nav::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "navbar-nav");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
ul id=[self.id()] class=[self.classes().get()] {
(items)
}
})
}
}
impl Nav {
// Nav BUILDER.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
pub fn with_item(mut self, item: navbar::Item) -> Self {
self.items.add(Child::with(item));
self
}
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
self.items.alter_typed(op);
self
}
// Nav GETTERS.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn items(&self) -> &Children {
&self.items
}
}