From 54f990b11c20f0ece3f84a4ceed9cb71f55e691f Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 26 Mar 2026 20:36:32 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoriza=20la=20API=20d?= =?UTF-8?q?e=20Children=20e=20InRegion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Patrón prototipo en `InRegion`: cada petición recibe clones profundos. - `ComponentClone` habilita clonar `dyn Component` de forma segura. - `ChildTyped` renombrado a `Slot`, elimina `ChildTypedOp`. - `Mutex` en lugar de `Arc` en `Child` y `Slot`. - `is_renderable` y `setup_before_prepare` reciben `&Context`. - Nuevos tests para `Children`, `ChildOp` y `Slot`. --- examples/navbar-menus.rs | 4 +- .../src/theme/container/component.rs | 6 +- .../src/theme/dropdown/component.rs | 22 +- .../src/theme/dropdown/item.rs | 6 +- .../src/theme/form/component.rs | 6 +- .../src/theme/form/fieldset.rs | 4 +- .../pagetop-bootsier/src/theme/form/input.rs | 6 +- extensions/pagetop-bootsier/src/theme/icon.rs | 8 +- .../src/theme/image/component.rs | 6 +- extensions/pagetop-bootsier/src/theme/nav.rs | 6 +- .../src/theme/nav/component.rs | 24 +- .../pagetop-bootsier/src/theme/nav/item.rs | 18 +- .../src/theme/navbar/brand.rs | 6 +- .../src/theme/navbar/component.rs | 46 ++- .../pagetop-bootsier/src/theme/navbar/item.rs | 18 +- .../src/theme/navbar/props.rs | 14 +- .../src/theme/offcanvas/component.rs | 6 +- src/base/action/component.rs | 14 +- .../component/transform_markup_component.rs | 2 +- src/base/component/block.rs | 6 +- src/base/component/html.rs | 22 +- src/base/component/intro.rs | 7 +- src/base/component/poweredby.rs | 4 +- src/core/component.rs | 11 +- src/core/component/children.rs | 210 +++++------- src/core/component/context.rs | 13 +- src/core/component/definition.rs | 78 +++-- src/core/component/error.rs | 5 +- src/core/theme/definition.rs | 2 +- src/core/theme/regions.rs | 144 ++++++-- src/response/page.rs | 2 +- tests/component_children.rs | 322 ++++++++++++++++++ tests/html_markup.rs | 6 +- 33 files changed, 740 insertions(+), 314 deletions(-) create mode 100644 tests/component_children.rs diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index a2ae76d0..11d6efc1 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -89,11 +89,11 @@ impl Extension for SuperMenu { })), )); - InRegion::Global(&DefaultRegion::Header).add(Child::with( + InRegion::Global(&DefaultRegion::Header).add( Container::new() .with_width(container::Width::FluidMax(UnitValue::RelRem(75.0))) .add_child(navbar_menu), - )); + ); } } diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index ad828c48..8deb7552 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -6,7 +6,7 @@ use crate::prelude::*; /// /// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza /// si existen componentes hijos (*children*). -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Container { #[getters(skip)] id: AttrId, @@ -29,11 +29,11 @@ impl Component for Container { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, self.container_width().to_class()); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let output = self.children().render(cx); if output.is_empty() { return Ok(html! {}); diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index cb721fca..988e94a3 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -19,7 +19,7 @@ use crate::LOCALES_BOOTSIER; /// /// Ver ejemplo en el módulo [`dropdown`]. /// Si no contiene elementos, el componente **no se renderiza**. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { #[getters(skip)] id: AttrId, @@ -56,14 +56,14 @@ impl Component for Dropdown { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes( ClassesOp::Prepend, self.direction().class_with(*self.button_grouped()), ); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { // Si no hay elementos en el menú, no se prepara. let items = self.items().render(cx); if items.is_empty() { @@ -247,10 +247,20 @@ impl Dropdown { self } - /// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`]. + /// Modifica la lista de elementos del menú aplicando una operación [`ChildOp`]. + /// + /// Para añadir elementos usa [`Child::with(item)`](Child::with): + /// + /// ```rust,ignore + /// dropdown.with_items(ChildOp::Add(Child::with(dropdown::Item::link(...)))); + /// dropdown.with_items(ChildOp::AddMany(vec![ + /// Child::with(dropdown::Item::link(...)), + /// Child::with(dropdown::Item::divider()), + /// ])); + /// ``` #[builder_fn] - pub fn with_items(mut self, op: TypedOp) -> Self { - self.items.alter_typed(op); + pub fn with_items(mut self, op: ChildOp) -> Self { + self.items.alter_child(op); self } } diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs index ac252d1b..379a16c9 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs @@ -7,7 +7,7 @@ use pagetop::prelude::*; /// /// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar /// con él. -#[derive(AutoDefault, Debug)] +#[derive(AutoDefault, Clone, Debug)] pub enum ItemKind { /// Elemento vacío, no produce salida. #[default] @@ -43,7 +43,7 @@ pub enum ItemKind { /// /// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { #[getters(skip)] id: AttrId, @@ -62,7 +62,7 @@ impl Component for Item { self.id.get() } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { Ok(match self.item_kind() { ItemKind::Void => html! {}, diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/extensions/pagetop-bootsier/src/theme/form/component.rs index 6ed05938..0dacfdea 100644 --- a/extensions/pagetop-bootsier/src/theme/form/component.rs +++ b/extensions/pagetop-bootsier/src/theme/form/component.rs @@ -27,7 +27,7 @@ use crate::theme::form; /// .with_classes(ClassesOp::Add, "mb-3") /// .add_child(Input::new().with_name("q")); /// ``` -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Form { #[getters(skip)] id: AttrId, @@ -48,11 +48,11 @@ impl Component for Form { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, "form"); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let method = match self.method() { form::Method::Post => Some("post"), form::Method::Get => None, diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index 536218e9..287e2655 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -3,7 +3,7 @@ use pagetop::prelude::*; /// Agrupa controles relacionados de un formulario (`
`). /// /// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { #[getters(skip)] id: AttrId, @@ -22,7 +22,7 @@ impl Component for Fieldset { self.id.get() } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { Ok(html! { fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] { @if let Some(legend) = self.legend().lookup(cx) { diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index 1872f806..409fc816 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -3,7 +3,7 @@ use pagetop::prelude::*; use crate::theme::form; use crate::LOCALES_BOOTSIER; -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Input { classes: Classes, input_type: form::InputType, @@ -29,14 +29,14 @@ impl Component for Input { Self::default() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes( ClassesOp::Prepend, util::join!("form-item form-type-", self.input_type().to_string()), ); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let id = self.name().get().map(|name| util::join!("edit-", name)); Ok(html! { div class=[self.classes().get()] { diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs index e3c0fe56..935aa847 100644 --- a/extensions/pagetop-bootsier/src/theme/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/icon.rs @@ -2,7 +2,7 @@ use crate::prelude::*; const DEFAULT_VIEWBOX: &str = "0 0 16 16"; -#[derive(AutoDefault)] +#[derive(AutoDefault, Clone)] pub enum IconKind { #[default] None, @@ -13,7 +13,7 @@ pub enum IconKind { }, } -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Icon { /// Devuelve las clases CSS asociadas al icono. classes: Classes, @@ -26,7 +26,7 @@ impl Component for Icon { Self::default() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { if !matches!(self.icon_kind(), IconKind::None) { self.alter_classes(ClassesOp::Prepend, "icon"); } @@ -35,7 +35,7 @@ impl Component for Icon { } } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { Ok(match self.icon_kind() { IconKind::None => html! {}, IconKind::Font(_) => { diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index f9f04d26..898fb573 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -9,7 +9,7 @@ use crate::prelude::*; /// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas** /// ([`classes::Rounded`](crate::theme::classes::Rounded)). /// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`]. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Image { #[getters(skip)] id: AttrId, @@ -32,11 +32,11 @@ impl Component for Image { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, self.source().to_class()); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let dimensions = self.size().to_style(); let alt_text = self.alternative().lookup(cx).unwrap_or_default(); let is_decorative = alt_text.is_empty(); diff --git a/extensions/pagetop-bootsier/src/theme/nav.rs b/extensions/pagetop-bootsier/src/theme/nav.rs index b5ae84a5..e6e2f813 100644 --- a/extensions/pagetop-bootsier/src/theme/nav.rs +++ b/extensions/pagetop-bootsier/src/theme/nav.rs @@ -19,9 +19,9 @@ //! .add_item(nav::Item::dropdown( //! Dropdown::new() //! .with_title(L10n::n("Options")) -//! .with_items(TypedOp::AddMany(vec![ -//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action".into())), -//! Typed::with(dropdown::Item::link(L10n::n("Another"), |_| "/another".into())), +//! .with_items(ChildOp::AddMany(vec![ +//! Child::with(dropdown::Item::link(L10n::n("Action"), |_| "/action".into())), +//! Child::with(dropdown::Item::link(L10n::n("Another"), |_| "/another".into())), //! ])), //! )) //! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into())); diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index fd849a9c..a0c82041 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -10,7 +10,7 @@ use crate::prelude::*; /// /// Ver ejemplo en el módulo [`nav`]. /// Si no contiene elementos, el componente **no se renderiza**. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Nav { #[getters(skip)] id: AttrId, @@ -33,7 +33,7 @@ impl Component for Nav { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, { let mut classes = "nav".to_string(); self.nav_kind().push_class(&mut classes); @@ -42,7 +42,7 @@ impl Component for Nav { }); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let items = self.items().render(cx); if items.is_empty() { return Ok(html! {}); @@ -108,10 +108,22 @@ impl Nav { self } - /// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`]. + /// Modifica la lista de elementos del menú aplicando una operación [`ChildOp`]. + /// + /// Para añadir elementos usa [`Child::with(item)`](Child::with): + /// + /// ```rust,ignore + /// nav.with_items(ChildOp::Add(Child::with(nav::Item::link(...)))); + /// nav.with_items(ChildOp::AddMany(vec![ + /// Child::with(nav::Item::link(...)), + /// Child::with(nav::Item::link_disabled(...)), + /// ])); + /// ``` + /// + /// Para la mayoría de los casos, [`add_item()`](Self::add_item) es más directo. #[builder_fn] - pub fn with_items(mut self, op: TypedOp) -> Self { - self.items.alter_typed(op); + pub fn with_items(mut self, op: ChildOp) -> Self { + self.items.alter_child(op); self } } diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index 45418021..454cff1a 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -10,7 +10,7 @@ use crate::LOCALES_BOOTSIER; /// /// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar /// con él. -#[derive(AutoDefault, Debug)] +#[derive(AutoDefault, Clone, Debug)] pub enum ItemKind { /// Elemento vacío, no produce salida. #[default] @@ -28,9 +28,9 @@ pub enum ItemKind { }, /// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del /// menú, sin añadir ningún comportamiento de navegación adicional. - Html(Typed), + Html(Slot), /// Elemento que despliega un menú [`Dropdown`]. - Dropdown(Typed), + Dropdown(Slot), } impl ItemKind { @@ -76,7 +76,7 @@ impl ItemKind { /// /// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { #[getters(skip)] id: AttrId, @@ -95,11 +95,11 @@ impl Component for Item { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class()); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { Ok(match self.item_kind() { ItemKind::Void => html! {}, @@ -159,7 +159,7 @@ impl Component for Item { }, ItemKind::Dropdown(menu) => { - if let Some(dd) = menu.borrow() { + if let Some(dd) = menu.get() { let items = dd.items().render(cx); if items.is_empty() { return Ok(html! {}); @@ -264,7 +264,7 @@ impl Item { /// con las clases de navegación asociadas a [`Item`]. pub fn html(html: Html) -> Self { Self { - item_kind: ItemKind::Html(Typed::with(html)), + item_kind: ItemKind::Html(Slot::with(html)), ..Default::default() } } @@ -276,7 +276,7 @@ impl Item { /// a su representación en [`Nav`]. pub fn dropdown(menu: Dropdown) -> Self { Self { - item_kind: ItemKind::Dropdown(Typed::with(menu)), + item_kind: ItemKind::Dropdown(Slot::with(menu)), ..Default::default() } } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index 5c22195a..ba8400f5 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -11,12 +11,12 @@ use crate::prelude::*; /// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título /// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza. /// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Brand { #[getters(skip)] id: AttrId, /// Devuelve la imagen de marca (si la hay). - image: Typed, + image: Slot, /// Devuelve el título de la identidad de marca. #[default(_code = "L10n::n(&global::SETTINGS.app.name)")] title: L10n, @@ -36,7 +36,7 @@ impl Component for Brand { self.id.get() } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { let image = self.image().render(cx); let title = self.title().using(cx); if title.is_empty() && image.is_empty() { diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 6f0ecf54..732097d4 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -14,7 +14,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Ver ejemplos en el módulo [`navbar`]. /// Si no contiene elementos, el componente **no se renderiza**. -#[derive(AutoDefault, Debug, Getters)] +#[derive(AutoDefault, Clone, Debug, Getters)] pub struct Navbar { #[getters(skip)] id: AttrId, @@ -39,7 +39,7 @@ impl Component for Navbar { self.id.get() } - fn setup_before_prepare(&mut self, _cx: &mut Context) { + fn setup(&mut self, _cx: &Context) { self.alter_classes(ClassesOp::Prepend, { let mut classes = "navbar".to_string(); self.expand().push_class(&mut classes, "navbar-expand", ""); @@ -48,7 +48,7 @@ impl Component for Navbar { }); } - fn prepare_component(&self, cx: &mut Context) -> Result { + fn prepare(&self, cx: &mut Context) -> Result { // Botón de despliegue (colapso u offcanvas) para la barra. fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup { let id_content_target = util::join!("#", id_content); @@ -133,7 +133,7 @@ impl Component for Navbar { @let id_content = offcanvas.id().unwrap_or_default(); (button(cx, TOGGLE_OFFCANVAS, &id_content)) - @if let Some(oc) = offcanvas.borrow() { + @if let Some(oc) = offcanvas.get() { (oc.render_offcanvas(cx, Some(self.items()))) } }, @@ -144,7 +144,7 @@ impl Component for Navbar { (brand.render(cx)) (button(cx, TOGGLE_OFFCANVAS, &id_content)) - @if let Some(oc) = offcanvas.borrow() { + @if let Some(oc) = offcanvas.get() { (oc.render_offcanvas(cx, Some(self.items()))) } }, @@ -155,7 +155,7 @@ impl Component for Navbar { (button(cx, TOGGLE_OFFCANVAS, &id_content)) (brand.render(cx)) - @if let Some(oc) = offcanvas.borrow() { + @if let Some(oc) = offcanvas.get() { (oc.render_offcanvas(cx, Some(self.items()))) } }, @@ -179,37 +179,37 @@ impl Navbar { /// Crea una barra de navegación **con marca a la izquierda**, siempre visible. pub fn simple_brand_left(brand: navbar::Brand) -> Self { - Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand))) + Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Slot::with(brand))) } /// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**. pub fn brand_left(brand: navbar::Brand) -> Self { - Self::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand))) + Self::default().with_layout(navbar::Layout::BrandLeft(Slot::with(brand))) } /// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**. pub fn brand_right(brand: navbar::Brand) -> Self { - Self::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand))) + Self::default().with_layout(navbar::Layout::BrandRight(Slot::with(brand))) } /// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**. pub fn offcanvas(oc: Offcanvas) -> Self { - Self::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc))) + Self::default().with_layout(navbar::Layout::Offcanvas(Slot::with(oc))) } /// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**. pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self { Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft( - Typed::with(brand), - Typed::with(oc), + Slot::with(brand), + Slot::with(oc), )) } /// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**. pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self { Self::default().with_layout(navbar::Layout::OffcanvasBrandRight( - Typed::with(brand), - Typed::with(oc), + Slot::with(brand), + Slot::with(oc), )) } @@ -262,10 +262,22 @@ impl Navbar { self } - /// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`]. + /// Modifica la lista de contenidos de la barra aplicando una operación [`ChildOp`]. + /// + /// Para añadir elementos usa [`Child::with(item)`](Child::with): + /// + /// ```rust,ignore + /// navbar.with_items(ChildOp::Add(Child::with(navbar::Item::nav(...)))); + /// navbar.with_items(ChildOp::AddMany(vec![ + /// Child::with(navbar::Item::nav(...)), + /// Child::with(navbar::Item::text(...)), + /// ])); + /// ``` + /// + /// Para la mayoría de los casos, [`add_item()`](Self::add_item) es más directo. #[builder_fn] - pub fn with_items(mut self, op: TypedOp) -> Self { - self.items.alter_typed(op); + pub fn with_items(mut self, op: ChildOp) -> Self { + self.items.alter_child(op); self } } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/item.rs b/extensions/pagetop-bootsier/src/theme/navbar/item.rs index 28e74cae..c75bcb9d 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/item.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/item.rs @@ -7,7 +7,7 @@ use crate::prelude::*; /// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del /// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son /// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`]. -#[derive(AutoDefault, Debug)] +#[derive(AutoDefault, Clone, Debug)] pub enum Item { /// Sin contenido, no produce salida. #[default] @@ -17,9 +17,9 @@ pub enum Item { /// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área /// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye, /// sólo añade otra dentro del bloque de contenidos. - Brand(Typed), + Brand(Slot), /// Representa un menú de navegación [`Nav`](crate::theme::Nav). - Nav(Typed