✨ (bootsier): Añade componentes esenciales
- Añade Dialog como modal de Bootstrap, con el JS de soporte para `htmx:confirm` y el saneado de su posición fija. - Adapta Nav, Navbar y Dropdown al reuso de los tipos movidos al core, y traduce BootsierColors/Intent en Button y Badge, con soporte para Light/Dark vía with_color(). - Reexporta en el tema los componentes de PageTop que faltaban: Block, Breadcrumb, Messages, Pager, Table y form::Number.
This commit is contained in:
parent
eb63a7ef37
commit
c0a5a8c3ab
47 changed files with 2135 additions and 1818 deletions
113
extensions/pagetop-bootsier/src/theme/bs/layout/region.rs
Normal file
113
extensions/pagetop-bootsier/src/theme/bs/layout/region.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Regiones específicas de la shell de Bootsier.
|
||||
pub enum BootsierRegions {
|
||||
/// Barra lateral de navegación (`app-sidebar` de AdminLTE).
|
||||
///
|
||||
/// Los componentes registrados aquí se renderizan directamente dentro del
|
||||
/// `<ul class="sidebar-menu">`, sin el `<div>` envolvente que añade
|
||||
/// [`Region`](pagetop::base::component::layout::Region) por defecto --
|
||||
/// [`Bootsier`](crate::Bootsier) intercepta este componente en `handle_component()` para
|
||||
/// renderizarlo así. Los elementos esperados son
|
||||
/// [`bs::sidebar::Item`](crate::theme::bs::sidebar::Item) y
|
||||
/// [`bs::sidebar::Section`](crate::theme::bs::sidebar::Section).
|
||||
///
|
||||
/// Sólo se renderiza en la plantilla de administración (`CoreTemplates::Admin`), que se
|
||||
/// activa creando la página con [`Page::admin()`](pagetop::response::Page::admin). Registrar
|
||||
/// elementos aquí no tiene efecto en páginas creadas con `Page::new()`.
|
||||
///
|
||||
/// # Registro global
|
||||
///
|
||||
/// Para que los ítems aparezcan en todas las páginas con shell, regístralos durante
|
||||
/// el arranque de la aplicación con [`InRegion::Global`]:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::bs::{BootsierRegions, sidebar};
|
||||
///
|
||||
/// InRegion::Global(&BootsierRegions::Sidebar)
|
||||
/// .add(sidebar::Section::titled(Lc::n("Administración")))
|
||||
/// .add(sidebar::Item::link(Lc::n("Usuarios"), "/users", "people"))
|
||||
/// .add(sidebar::Item::link(Lc::n("Roles"), "/roles", "shield-check"));
|
||||
/// ```
|
||||
///
|
||||
/// # Registro por página
|
||||
///
|
||||
/// Para añadir ítems sólo en una página concreta, usa [`Contextual::with_child_in`]:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::bs::{BootsierRegions, sidebar};
|
||||
///
|
||||
/// async fn dashboard(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
/// Page::admin(request)
|
||||
/// .with_child_in(
|
||||
/// &BootsierRegions::Sidebar,
|
||||
/// sidebar::Item::link(Lc::n("Panel"), "/dashboard", "grid"),
|
||||
/// )
|
||||
/// .render().await
|
||||
/// }
|
||||
/// ```
|
||||
Sidebar,
|
||||
|
||||
/// Elementos adicionales en la barra de navegación superior (`app-header`).
|
||||
///
|
||||
/// Los componentes registrados aquí se renderizan en el lado derecho de la barra superior,
|
||||
/// a continuación de los controles fijos (pantalla completa y selector de tema). Los elementos
|
||||
/// esperados son típicamente ítems de navegación (`<li class="nav-item">`).
|
||||
///
|
||||
/// Esta región es opcional: si no tiene contenido, no añade ningún marcado al navbar.
|
||||
///
|
||||
/// # Registro global
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::bs::BootsierRegions;
|
||||
///
|
||||
/// InRegion::Global(&BootsierRegions::Navbar)
|
||||
/// .add(Html::with(|_| html! {
|
||||
/// li class="nav-item" {
|
||||
/// a class="nav-link" href="/logout" { "Cerrar sesión" }
|
||||
/// }
|
||||
/// }));
|
||||
/// ```
|
||||
Navbar,
|
||||
}
|
||||
|
||||
impl RegionName for BootsierRegions {
|
||||
#[inline]
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Sidebar => "bootsier-sidebar",
|
||||
Self::Navbar => "bootsier-navbar",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn label(&self) -> Lc {
|
||||
match self {
|
||||
Self::Sidebar => Lc::t("region_sidebar", &LOCALES_BOOTSIER),
|
||||
Self::Navbar => Lc::t("region_navbar", &LOCALES_BOOTSIER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Region RENDER >******************************************************************************
|
||||
|
||||
// Regiones de Bootsier: se renderizan sin el `<div role="region">` envolvente que aplica
|
||||
// `layout::Region::prepare()` por defecto -- sus elementos van directamente dentro del contenedor
|
||||
// que los gestiona (sidebar-menu o navbar-nav). Devuelve `None` si `component` no envuelve una
|
||||
// `BootsierRegions`, dejando que el resto de la cadena de temas (o el propio componente) resuelva
|
||||
// el renderizado por defecto.
|
||||
pub(crate) async fn render(
|
||||
component: &layout::Region,
|
||||
cx: &mut Context,
|
||||
) -> Option<Result<Markup, ComponentError>> {
|
||||
match component.region().downcast_ref::<BootsierRegions>()? {
|
||||
BootsierRegions::Sidebar | BootsierRegions::Navbar => {
|
||||
Some(Ok(cx.render_region(component.region()).await))
|
||||
}
|
||||
}
|
||||
}
|
||||
170
extensions/pagetop-bootsier/src/theme/bs/layout/template.rs
Normal file
170
extensions/pagetop-bootsier/src/theme/bs/layout/template.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::config;
|
||||
use crate::theme::{ContainerBootsier, bs};
|
||||
use crate::{ADMINLTE_VERSION, LOCALES_BOOTSIER};
|
||||
|
||||
// Regiones de Bootsier: se renderiza sin el `<div role="region">` envolvente que aplica
|
||||
// `layout::Template::prepare()` por defecto -- delega en `render_standard()`/`render_admin()`
|
||||
// según la variante de `CoreTemplates` que envuelva el componente. Devuelve `None` si
|
||||
// `component` no envuelve una `CoreTemplates`, dejando que el resto de la cadena de temas (o
|
||||
// el propio componente) resuelva el renderizado por defecto.
|
||||
pub(crate) async fn render(
|
||||
component: &layout::Template,
|
||||
cx: &mut Context,
|
||||
) -> Option<Result<Markup, ComponentError>> {
|
||||
match component.template().downcast_ref::<CoreTemplates>()? {
|
||||
CoreTemplates::Standard => Some(Ok(render_standard(cx).await)),
|
||||
CoreTemplates::Admin => Some(Ok(render_admin(cx).await)),
|
||||
}
|
||||
}
|
||||
|
||||
// Layout estándar: `CoreRegions::Header`, `CoreRegions::Aside`, `CoreRegions::Content` y
|
||||
// `CoreRegions::Footer` envueltos en un contenedor de ancho configurable.
|
||||
async fn render_standard(cx: &mut Context) -> Markup {
|
||||
bs::Container::new()
|
||||
.with_prop(PropsOp::add_classes("container-wrapper"))
|
||||
.with_width(bs::container::Width::FluidMax(
|
||||
config::SETTINGS.bootsier.max_width,
|
||||
))
|
||||
.with_child(layout::Region::header())
|
||||
.with_child(layout::Region::aside())
|
||||
.with_child(layout::Region::default())
|
||||
.with_child(layout::Region::footer())
|
||||
.render(cx)
|
||||
.await
|
||||
}
|
||||
|
||||
// Layout de administración: shell de AdminLTE 4 (barra superior, barra lateral con el contenido
|
||||
// de BootsierRegions::Sidebar, área de contenido y pie).
|
||||
async fn render_admin(cx: &mut Context) -> Markup {
|
||||
cx.alter_body_props(PropsOp::add_classes(
|
||||
"layout-fixed sidebar-expand-lg bg-body-tertiary",
|
||||
));
|
||||
cx.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootsier.shell.min.js")
|
||||
.with_version(ADMINLTE_VERSION)
|
||||
.with_weight(-88),
|
||||
));
|
||||
// `CoreRegions::Aside` es una región neutra del core: la usa `pagetop-admin` para su menú de
|
||||
// secciones sin que este tema tenga que depender de él. `BootsierRegions::Sidebar` sigue
|
||||
// disponible para que cualquier extensión añada elementos propios a mano.
|
||||
let aside = layout::Region::of(&CoreRegions::Aside).render(cx).await;
|
||||
let sidebar = layout::Region::of(&bs::BootsierRegions::Sidebar)
|
||||
.render(cx)
|
||||
.await;
|
||||
render_shell(cx, html! { (aside) (sidebar) }).await
|
||||
}
|
||||
|
||||
async fn render_shell(cx: &mut Context, sidebar: Markup) -> Markup {
|
||||
let navbar = layout::Region::of(&bs::BootsierRegions::Navbar)
|
||||
.render(cx)
|
||||
.await;
|
||||
let content = layout::Region::default().render(cx).await;
|
||||
let footer = layout::Region::footer().render(cx).await;
|
||||
html! {
|
||||
div class="app-wrapper" {
|
||||
// Barra de navegación superior (app-header)
|
||||
nav class="app-header navbar navbar-expand bg-body" {
|
||||
div class="container-fluid" {
|
||||
ul class="navbar-nav" {
|
||||
li class="nav-item" {
|
||||
a class="nav-link" data-lte-toggle="sidebar" href="#" role="button" {
|
||||
i class="bi bi-list" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul class="navbar-nav ms-auto" {
|
||||
// Botón de pantalla completa
|
||||
li class="nav-item" {
|
||||
a class="nav-link" href="#" data-lte-toggle="fullscreen"
|
||||
aria-label=[Lc::t("shell_fullscreen", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{
|
||||
i data-lte-icon="maximize" class="bi bi-fullscreen" {}
|
||||
i data-lte-icon="minimize" class="bi bi-fullscreen-exit d-none" {}
|
||||
}
|
||||
}
|
||||
// Selector de modo de color (claro / oscuro / automático)
|
||||
li class="nav-item dropdown" {
|
||||
a class="nav-link" href="#" id="bd-theme"
|
||||
data-bs-toggle="dropdown" aria-expanded="false"
|
||||
aria-label=[Lc::t("shell_theme_toggle", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{
|
||||
i class="bi bi-sun-fill" data-lte-theme-icon="light" {}
|
||||
i class="bi bi-moon-fill d-none" data-lte-theme-icon="dark" {}
|
||||
i class="bi bi-circle-half d-none" data-lte-theme-icon="auto" {}
|
||||
}
|
||||
ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme"
|
||||
style="--bs-dropdown-min-width: 8rem"
|
||||
{
|
||||
li {
|
||||
button type="button"
|
||||
class="dropdown-item d-flex align-items-center"
|
||||
data-bs-theme-value="light"
|
||||
aria-pressed="false"
|
||||
{
|
||||
i class="bi bi-sun-fill me-2" {}
|
||||
(Lc::t("shell_theme_light", &LOCALES_BOOTSIER).using(cx))
|
||||
i class="bi bi-check-lg ms-auto d-none" {}
|
||||
}
|
||||
}
|
||||
li {
|
||||
button type="button"
|
||||
class="dropdown-item d-flex align-items-center"
|
||||
data-bs-theme-value="dark"
|
||||
aria-pressed="false"
|
||||
{
|
||||
i class="bi bi-moon-fill me-2" {}
|
||||
(Lc::t("shell_theme_dark", &LOCALES_BOOTSIER).using(cx))
|
||||
i class="bi bi-check-lg ms-auto d-none" {}
|
||||
}
|
||||
}
|
||||
li {
|
||||
button type="button"
|
||||
class="dropdown-item d-flex align-items-center"
|
||||
data-bs-theme-value="auto"
|
||||
aria-pressed="false"
|
||||
{
|
||||
i class="bi bi-circle-half me-2" {}
|
||||
(Lc::t("shell_theme_auto", &LOCALES_BOOTSIER).using(cx))
|
||||
i class="bi bi-check-lg ms-auto d-none" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(navbar)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Barra lateral (app-sidebar)
|
||||
aside class="app-sidebar bg-body-secondary shadow" data-bs-theme="dark" {
|
||||
div class="sidebar-brand" {
|
||||
a href="/" class="brand-link" {
|
||||
span class="brand-text fw-light" { (global::SETTINGS.app.name) }
|
||||
}
|
||||
}
|
||||
div class="sidebar-wrapper" {
|
||||
nav class="mt-2" {
|
||||
ul class="nav sidebar-menu flex-column"
|
||||
data-lte-toggle="treeview" role="menu"
|
||||
{
|
||||
(sidebar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Área de contenido principal (app-main)
|
||||
main class="app-main" {
|
||||
div class="app-content" {
|
||||
div class="container-fluid" {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pie de página (app-footer)
|
||||
footer class="app-footer" {
|
||||
(footer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue