Compare commits

..

No commits in common. "main" and "legacy/latest" have entirely different histories.

709 changed files with 27867 additions and 20898 deletions

View file

@ -1,69 +0,0 @@
# cliff.toml
[changelog]
header = """
# CHANGELOG
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
internos pueden omitirse si no afectan al uso del proyecto.
"""
trim = true
render_always = true
body = """
{% if version %}
## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}
## Pendiente de publicación
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{%- set msg = commit.message
| split(pat="\n")
| first
| replace(from="✨ ", to="")
| replace(from="🐛 ", to="")
| replace(from="🚑 ", to="")
| replace(from="⬆️ ", to="")
| replace(from="🚧 ", to="")
| replace(from="♻️ ", to="")
| replace(from="✏️ ", to="")
| replace(from="🏷️ ", to="")
| replace(from="🧑‍💻 ", to="")
| replace(from="🍱 ", to="")
| replace(from="📝 ", to="")
| replace(from="💡 ", to="")
-%}
- {{ msg | trim }} {% if commit.author.name != "Manuel Cillero" %} - {{ commit.author.name }}{% endif %}
{% endfor %}{% endfor %}
"""
[git]
conventional_commits = false
filter_unconventional = false
topo_order_commits = true
sort_commits = "oldest"
commit_parsers = [
{ message = "^✨", group = "Añadido" },
{ message = "^🐛", group = "Corregido" },
{ message = "^🚑", group = "Corregido" },
{ message = "^🚧", group = "Cambiado" },
{ message = "^♻️", group = "Cambiado" },
{ message = "^✏️", group = "Cambiado" },
{ message = "^🏷️", group = "Cambiado" },
{ message = "^🧑‍💻", group = "Cambiado" },
{ message = "^🍱", group = "Cambiado" },
{ message = "^⬆️", group = "Dependencias" },
{ message = "^📝", group = "Documentado" },
{ message = "^💡", group = "Documentado" },
{ message = "^.*", group = "Otros cambios" },
]

View file

@ -1,3 +0,0 @@
[alias]
ts = ["test", "--features", "testing"] # cargo ts
tw = ["test", "--workspace", "--features", "testing"] # cargo tw

View file

@ -1,25 +0,0 @@
# release.toml
# Etiqueta por crate: `pagetop-macros-v0.2.0`
tag-prefix = "{{crate_name}}-"
# Confirmaciones firmadas (no requeridas)
sign-commit = false
sign-tag = false
# Empuja etiquetas y commits
push = true
# Publica en crates.io (puedes desactivarlo para pruebas)
publish = true
# Solo permite publicar estos crates (los que forman parte del workspace)
allow-branch = ["main"]
consolidate-commits = false
# Mensaje personalizado para el commit de versión
pre-release-commit-message = "🔖 Prepara publicación de {{crate_name}} {{version}}"
pre-release-hook = [
"sh", "-c", "ROOT=$(git rev-parse --show-toplevel) && \"$ROOT/tools/changelog.sh\" {{crate_name}} {{version}} --stage"
]

3
.gitignore vendored
View file

@ -8,3 +8,6 @@
**/local.*.toml
**/local.toml
.env
# Directorio de trabajo
workdir

View file

@ -1,86 +0,0 @@
# CHANGELOG
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
internos pueden omitirse si no afectan al uso del proyecto.
## 0.4.0 (2025-09-20)
### Añadido
- [app] Añade manejo de rutas no encontradas
- [context] Añade métodos auxiliares de parámetros
- [util] Añade `indoc` para indentar código bien
- Añade componente `PoweredBy` para copyright
### Cambiado
- [html] Cambia tipos `Option...` por `Attr...`
- [html] Implementa `Default` en `Context`
- [welcome] Crea página de bienvenida desde intro
- [context] Generaliza los parámetros de contexto
- [context] Define un `trait` común de contexto
- Modifica tipos para atributos HTML a minúsculas
- Renombra `with_component` por `add_child`
### Corregido
- [welcome] Corrige giro botón con ancho estrecho
- [welcome] Corrige centrado del pie de página
- Corrige nombre de función en prueba de `Html`
- Corrige doc y código por cambios en Page
### Dependencias
- Actualiza dependencias para 0.4.0
### Documentado
- [component] Amplía documentación de preparación
- Normaliza referencias al nombre PageTop
- Simplifica documentación de obsoletos
- Mejora la documentación de recursos y contexto
### Otros cambios
- 🎨 [theme] Mejora gestión de regiones en páginas
- ✅ [tests] Amplía pruebas para `PrepareMarkup'
- 🎨 [locale] Mejora el uso de `lookup` / `using`
- 🔨 [tools] Fuerza pulsar intro para confirmar input
- 💄 Aplica BEM a estilos de bienvenida y componente
- 🎨 Unifica conversiones a String con `to_string()`
- 🔥 Elimina `Render` para usar siempre el contexto
## 0.3.0 (2025-08-16)
### Cambiado
- Redefine función para directorios absolutos
- Mejora la integración de archivos estáticos
### Documentado
- Cambia el formato para la documentación
## 0.2.0 (2025-08-09)
### Añadido
- Añade librería para gestionar recursos estáticos
- Añade soporte a changelog de `pagetop-statics`
### Documentado
- Corrige enlace del botón de licencia en la documentación
### Otros cambios
- Afina Cargo.toml para buscar la mejor categoría
## 0.1.0 (2025-08-06)
- Versión inicial

View file

@ -1,23 +1,31 @@
# 🔃 Dependencias
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
`PageTop` está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes
aprovechando algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs)
como:
* [Actix Web](https://actix.rs/) para los servicios web.
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
de la aplicación.
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
PageTop y sus extensiones.
* Además de otros crates adicionales que puedes explorar en los archivos `Cargo.toml` de `PageTop`
y sus extensiones.
# ⌨️ Código
`PageTop` incorpora código de [config-rs](https://crates.io/crates/config) (versión
[0.11.0](https://github.com/mehcode/config-rs/tree/0.11.0)) de
[Ryan Leckey](https://crates.io/users/mehcode), por sus ventajas para leer y asignar a tipos seguros
las opciones de configuración, delegando la asignación a cada extensión, tema o aplicación.
# 🗚 FIGfonts
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
`PageTop` usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por
*yuanbohan* para mostrar un banner de presentación en el terminal con el nombre de la aplicación en
caracteres [FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
@ -26,6 +34,14 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
# 📰 Plantillas
La página de inicio predeterminada está inspirada en este práctico
[tutorial](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript)
realizado por [Faraz](https://www.codewithfaraz.com/) que crea una página de demostración en estilo
*Neobrutalismo*.
# 🎨 Icono
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).

2932
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,90 +1,50 @@
[package]
name = "pagetop"
version = "0.4.0"
edition = "2021"
description = """
Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
"""
categories = ["web-programming::http-server"]
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
chrono = "0.4"
colored = "3.0"
concat-string = "1.0"
config = { version = "0.15", default-features = false, features = ["toml"] }
figlet-rs = "0.1"
indoc = "2.0"
itoa = "1.0"
parking_lot = "0.12"
paste = { package = "pastey", version = "0.1" }
substring = "1.4"
terminal_size = "0.4"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
tracing-actix-web = "0.7"
fluent-templates = "0.13"
unic-langid = { version = "0.9", features = ["macros"] }
actix-web = { workspace = true, default-features = true }
actix-session = { version = "0.11", features = ["cookie-session"] }
actix-web-files = { package = "actix-files", version = "0.6" }
serde.workspace = true
pagetop-macros.workspace = true
pagetop-statics.workspace = true
[features]
default = []
testing = []
[dev-dependencies]
tempfile = "3.23"
serde_json = "1.0"
pagetop-aliner.workspace = true
pagetop-bootsier.workspace = true
[build-dependencies]
pagetop-build.workspace = true
[workspace]
resolver = "2"
members = [
# Helpers
"helpers/pagetop-build",
"helpers/pagetop-macros",
"helpers/pagetop-statics",
# PageTop
"pagetop",
# Extensions
"extensions/pagetop-aliner",
"extensions/pagetop-seaorm",
"extensions/pagetop-mdbook",
"extensions/pagetop-hljs",
# Themes
#"extensions/pagetop-aliner",
"extensions/pagetop-bootsier",
# Apps
"website",
"drust",
]
[workspace.package]
repository = "https://git.cillero.es/manuelcillero/pagetop"
repository = "https://github.com/manuelcillero/pagetop"
homepage = "https://pagetop.cillero.es"
license = "MIT OR Apache-2.0"
authors = ["Manuel Cillero <manuel@cillero.es>"]
[workspace.dependencies]
actix-web = { version = "4.11", default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
static-files = "0.2.4"
# Helpers
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
# Extensions
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
# PageTop
pagetop = { version = "0.4", path = "." }
pagetop = { version = "0.0", path = "pagetop" }
# Extensions
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
pagetop-mdbook = { version = "0.0", path = "extensions/pagetop-mdbook" }
pagetop-hljs = { version = "0.0", path = "extensions/pagetop-hljs" }
# Themes
#pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }

View file

@ -1,21 +1,20 @@
<div align="center">
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
<h1>PageTop</h1>
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
<p>Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.</p>
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
[![Doc API](https://img.shields.io/docsrs/pagetop?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop)
[![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop)
[![Descargas](https://img.shields.io/crates/d/pagetop.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop#licencia)
<br>
</div>
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
la creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
según las necesidades de cada proyecto, incluyendo:
@ -24,16 +23,16 @@ según las necesidades de cada proyecto, incluyendo:
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
configurables y reutilizables.
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
de PageTop o de terceros.
de `PageTop` o de terceros.
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
componentes sin comprometer su funcionalidad.
# ⚡️ Guía rápida
La aplicación más sencilla de PageTop se ve así:
La aplicación más sencilla de `PageTop` se ve así:
```rust,no_run
```rust#ignore
use pagetop::prelude::*;
#[pagetop::main]
@ -42,17 +41,17 @@ async fn main() -> std::io::Result<()> {
}
```
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
Por defecto, este código sirve una página web de bienvenida accesible desde un navegador en la
dirección `http://localhost:8088`, siguiendo la configuración predeterminada.
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
Para personalizar el servicio, puedes crear una extensión de `PageTop` de la siguiente manera:
```rust,no_run
```rust#ignore
use pagetop::prelude::*;
struct HelloWorld;
impl Extension for HelloWorld {
impl ExtensionTrait for HelloWorld {
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
scfg.route("/", service::web::get().to(hello_world));
}
@ -60,7 +59,7 @@ impl Extension for HelloWorld {
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
.with_component(Html::with(html! { h1 { "Hello world!" } }))
.render()
}
@ -76,58 +75,57 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
# 📂 Repositorio
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
El código se organiza en un *workspace* con los siguientes subproyectos:
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
* **[pagetop](https://github.com/manuelcillero/pagetop/tree/latest/pagetop)**, es la librería
principal. Reúne algunos de los *crates* más estables y populares del ecosistema Rust para
proporcionar APIs y recursos para la creación avanzada de soluciones web.
## Auxiliares
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
compilación.
* **[pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build)**,
permite incluir fácilmente archivos estáticos o archivos SCSS compilados directamente en el
binario de las aplicaciones `PageTop`.
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
aplicaciones PageTop durante la compilación de los ejecutables.
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
* **[pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros)**,
proporciona una colección de macros que mejoran la experiencia de desarrollo con `PageTop`.
## Extensiones
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
* **[pagetop-seaorm](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-seaorm)**,
integra [SeaORM](https://www.sea-ql.org/SeaORM) para trabajar con bases de datos en aplicaciones
`PageTop`.
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
componentes flexibles.
* **[pagetop-mdbook](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-mdbook)**,
incluye contenido generado por [mdBook](https://rust-lang.github.io/mdBook/) en aplicaciones
desarrolladas con `PageTop`.
* **[pagetop-hljs](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-hljs)**,
utiliza [HighlightJS](https://highlightjs.org) para mostrar fragmentos de código con resaltado
de sintaxis con `PageTop`.
# 🧪 Pruebas
## Temas
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
* **[pagetop-bootsier](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-bootsier)**,
tema para `PageTop` que usa [Bootstrap](https://getbootstrap.com) para dar vida a tus diseños
web.
| Comando | Descripción |
| ------- | ----------- |
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
## Aplicaciones
> **Nota**
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
* **[drust](https://github.com/manuelcillero/pagetop/tree/latest/drust)**, es una aplicación que
utiliza `PageTop` para crear un Sistema de Gestión de Contenidos (CMS) que permita construir
sitios web dinámicos, administrados y configurables.
* **[website](https://github.com/manuelcillero/pagetop/tree/latest/website)**, es la aplicación
web creada con el propio entorno `PageTop` para descubrir a la comunidad su ecosistema en
[pagetop.cillero.es](https://pagetop.cillero.es).
# 🚧 Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Su API es
inestable y está sujeta a cambios frecuentes. No recomiendo su uso en producción, al menos mientras
no se libere una versión **1.0.0**.
# 📜 Licencia
@ -144,7 +142,7 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
el ecosistema Rust.
# ✨ Contribuir
# ✨ Contribuciones
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la

View file

@ -1,2 +0,0 @@
[log]
tracing = "Info,pagetop=Debug"

22
drust/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "drust"
version = "0.0.5"
edition = "2021"
description = """\
Un Sistema de Gestión de Contenidos (CMS) basado en PageTop para compartir tu mundo.\
"""
default-run = "drust"
repository.workspace = true
homepage.workspace = true
license.workspace = true
authors.workspace = true
[dependencies]
pagetop.workspace = true
pagetop-bootsier.workspace = true
#pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
#pagetop-user = { version = "0.0", path = "../pagetop-user" }
#pagetop-node = { version = "0.0", path = "../pagetop-node" }

112
drust/README.md Normal file
View file

@ -0,0 +1,112 @@
<div align="center">
<h1>Drust</h1>
<p>Un Sistema de Gestión de Contenidos (CMS) basado en <strong>PageTop</strong> para compartir tu mundo.</p>
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
[![Crates.io](https://img.shields.io/crates/v/drust.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/drust)
[![Descargas](https://img.shields.io/crates/d/drust.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/drust)
</div>
## Descripción general
`Drust` exprime `PageTop` para desarrollar un *Sistema de Gestión de Contenidos* (CMS) básico,
modestamente inspirado en [Drupal](https://www.drupal.org), que permita construir sitios web
dinámicos, manejables y personalizables; y facilite a los usuarios la gestión de una variedad de
contenidos de manera sencilla.
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
# ⚡️ Guía rápida
`Drust` requiere una base de datos para funcionar. La aplicación se encarga de ejecutar las
migraciones y cargar los datos mínimos necesarios, pero para crear o borrar la base de datos puedes
usar los scripts `db-create.sh` y `db-delete.sh` que se encuentran en el directorio `tools` del
*workspace*.
## Configuración de `.env`
Para simplificar la configuración, en el directorio `tools` puedes crear un archivo `.env` para
definir las variables de entorno que requieren los scripts para gestionar la base de datos, aunque
su presencia es **opcional**. Si no se encuentra `.env` o carece de ciertos valores, los scripts
solicitarán las variables necesarias para su ejecución.
> **Nota**: Evita usar caracteres especiales como `@`, `#`, `?`, `:` en `DB_PASS` para prevenir
> posibles problemas de interpretación de `DATABASE_URL` en el código.
### Ejemplo de `.env`
```bash
# Sistema de base de datos
DB_SYSTEM="psql"
# Nombre del host
DB_HOST="localhost"
# Puerto de conexión
DB_PORT="5432"
# Nombre de la base de datos
DB_NAME="drust"
# Usuario de la base de datos
DB_USER="drust"
# Contraseña para el usuario de la base de datos
# Evita usar caracteres especiales como '@', '#', '?', ':', ';' o espacios
DB_PASS="password"
# Usuario administrador
DB_ADMIN="postgres"
# Contraseña del usuario administrador
DB_ADMIN_PASS="adminpassword"
```
## Ejecución de los scripts
Asegúrate de que los scripts tienen permisos de ejecución:
```bash
chmod +x db-create.sh db-delete.sh
```
Y ejecuta el script deseado:
```bash
./db-create.sh
```
o
```bash
./db-delete.sh
```
# 🚧 Advertencia
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Su API es
inestable y está sujeta a cambios frecuentes. No recomiendo su uso en producción, al menos mientras
no se libere una versión **1.0.0**.
# 📜 Licencia
El código está disponible bajo una doble licencia:
* **Licencia MIT**
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
* **Licencia Apache, Versión 2.0**
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
el ecosistema Rust.

6
drust/config/common.toml Normal file
View file

@ -0,0 +1,6 @@
[app]
name = "Drust"
description = "A modern web Content Management System to share your world."
[database]
db_type = "mysql"

View file

@ -0,0 +1,7 @@
[app]
#theme = "Aliner"
theme = "Bootsier"
language = "es-ES"
[log]
tracing = "Info,pagetop=Debug,sqlx::query=Warn"

View file

@ -0,0 +1,40 @@
[app]
name = "Sample"
description = "Developed with the amazing PageTop framework."
# Default theme.
theme = "Default"
# Default language (localization).
language = "en-US"
# Default text direction: "ltr", "rtl", or "auto".
direction = "ltr"
# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars".
startup_banner = "Slant"
[dev]
# Static files required by the app are integrated by default into the executable
# binary. However, during development, it can be useful to serve these files
# from their own directory to avoid recompiling every time they are modified. In
# this case, just indicate the full path to the project's root directory.
pagetop_project_dir = ""
[log]
# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace".
# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
tracing = "Info"
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
rolling = "Stdout"
# Directory for trace files (if rolling != "Stdout").
path = "log"
# Prefix for trace files (if rolling != "Stdout").
prefix = "tracing.log"
# Traces format: "Full", "Compact", "Pretty", or "Json".
format = "Full"
[server]
# Web server config.
bind_address = "localhost"
bind_port = 8088
# Session cookie duration (in seconds), i.e., the time from when the session is
# created until the cookie expires. A value of 0 indicates "until the browser is
# closed". By default, it is one week.
session_lifetime = 604800

39
drust/src/main.rs Normal file
View file

@ -0,0 +1,39 @@
//! <div align="center">
//!
//! <h1>Drust</h1>
//!
//! <p>Un Sistema de Gestión de Contenidos (CMS) basado en <strong>PageTop</strong> para compartir tu mundo.</p>
//!
//! [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
//! [![Crates.io](https://img.shields.io/crates/v/drust.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/drust)
//! [![Descargas](https://img.shields.io/crates/d/drust.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/drust)
//!
//! </div>
//!
//! ## Sobre PageTop
//!
//! [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la
//! web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles
//! y configurables, basadas en HTML, CSS y JavaScript.
use pagetop::prelude::*;
struct Drust;
impl ExtensionTrait for Drust {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
// Extensiones.
//&pagetop_admin::Admin,
//&pagetop_user::User,
//&pagetop_node::Node,
// Temas.
&pagetop_bootsier::Bootsier,
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&Drust).run()?.await
}

View file

@ -1,109 +0,0 @@
use pagetop::prelude::*;
use pagetop_bootsier::prelude::*;
struct SuperMenu;
impl Extension for SuperMenu {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
}
fn initialize(&self) {
let home_path = |cx: &Context| match cx.langid().language.as_str() {
"en" => "/en",
_ => "/",
};
let navbar_menu = Navbar::brand_left(navbar::Brand::new().with_path(Some(home_path)))
.with_expand(BreakPoint::LG)
.add_item(navbar::Item::nav(
Nav::new()
.add_item(nav::Item::link(
L10n::l("sample_menus_item_link"),
home_path,
))
.add_item(nav::Item::link_blank(
L10n::l("sample_menus_item_blank"),
|_| "https://docs.rs/pagetop",
))
.add_item(nav::Item::dropdown(
Dropdown::new()
.with_title(L10n::l("sample_menus_test_title"))
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_dev_getting_started"),
|_| "/dev/getting-started",
))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_dev_guides"),
|_| "/dev/guides",
))
.add_item(dropdown::Item::link_blank(
L10n::l("sample_menus_dev_forum"),
|_| "https://forum.example.dev",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_sdk_rust"),
|_| "/dev/sdks/rust",
))
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
"/dev/sdks/js"
}))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_sdk_python"),
|_| "/dev/sdks/python",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::header(L10n::l(
"sample_menus_plugin_header",
)))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_plugin_auth"),
|_| "/dev/sdks/rust/plugins/auth",
))
.add_item(dropdown::Item::link(
L10n::l("sample_menus_plugin_cache"),
|_| "/dev/sdks/rust/plugins/cache",
))
.add_item(dropdown::Item::divider())
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
.add_item(dropdown::Item::link_disabled(
L10n::l("sample_menus_item_disabled"),
|_| "#",
)),
))
.add_item(nav::Item::link_disabled(
L10n::l("sample_menus_item_disabled"),
|_| "#",
)),
))
.add_item(navbar::Item::nav(
Nav::new()
.with_classes(
ClassesOp::Add,
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
)
.add_item(nav::Item::link(
L10n::l("sample_menus_item_sign_up"),
|_| "/auth/sign-up",
))
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
"/auth/login"
})),
));
InRegion::Named("header").add(Child::with(
Container::new()
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
.add_child(navbar_menu),
));
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&SuperMenu).run()?.await
}

1
extensions/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
pagetop-aliner/**

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Manuel Cillero
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,101 +0,0 @@
<div align="center">
<h1>PageTop Aliner</h1>
<p>Tema de <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
[![Doc API](https://img.shields.io/docsrs/pagetop-aliner?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-aliner)
[![Crates.io](https://img.shields.io/crates/v/pagetop-aliner.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-aliner)
[![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
<br>
</div>
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
# ⚡️ Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
```toml
[dependencies]
pagetop-aliner = "..."
```
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
```rust,no_run
use pagetop::prelude::*;
struct MyApp;
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
// ...
&pagetop_aliner::Aliner,
// ...
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
```toml
[app]
theme = "Aliner"
```
…o **fuerza el tema por código** en una página concreta:
```rust,no_run
use pagetop::prelude::*;
use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Aliner)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
})),
)
.render()
}
```
# 🚧 Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
# 📜 Licencia
El código está disponible bajo una doble licencia:
* **Licencia MIT**
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
* **Licencia Apache, Versión 2.0**
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
el ecosistema Rust.

View file

@ -1,115 +0,0 @@
/*!
<div align="center">
<h1>PageTop Aliner</h1>
<p>Tema para <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
[![Doc API](https://img.shields.io/docsrs/pagetop-aliner?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-aliner)
[![Crates.io](https://img.shields.io/crates/v/pagetop-aliner.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-aliner)
[![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
<br>
</div>
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
# Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
```toml
[dependencies]
pagetop-aliner = "..."
```
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
```rust,no_run
use pagetop::prelude::*;
struct MyApp;
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
// ...
&pagetop_aliner::Aliner,
// ...
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
```toml
[app]
theme = "Aliner"
```
o **fuerza el tema por código** en una página concreta:
```rust,no_run
use pagetop::prelude::*;
use pagetop_aliner::Aliner;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Aliner)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
})),
)
.render()
}
```
*/
use pagetop::prelude::*;
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
///
/// Define un tema mínimo útil para:
///
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
/// - Realizar pruebas de renderizado rápido con salida estable y predecible.
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
pub struct Aliner;
impl Extension for Aliner {
fn theme(&self) -> Option<ThemeRef> {
Some(&Self)
}
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, [aliner] => "/aliner");
}
}
impl Theme for Aliner {
fn after_render_page_body(&self, page: &mut Page) {
page.alter_param("include_basic_assets", true)
.alter_assets(ContextOp::AddStyleSheet(
StyleSheet::from("/aliner/css/styles.css")
.with_version(env!("CARGO_PKG_VERSION"))
.with_weight(-90),
));
}
}

View file

@ -1,356 +0,0 @@
html {
background-color: white;
padding: 1px 3px;
}
body {
padding: 1px 3px;
}
div {
padding: 1px 3px;
margin: 5px;
}
h1, h2, h3, h4,h5, h6, p {
background-color: snow;
}
* * {
outline: 5px solid rgba(255,0,0,.1);
}
* * * {
outline: 3px dashed rgba(255,0,0,.4);
}
* * * * {
outline: 2px dotted rgba(255,0,0,.6);
}
* * * * * {
outline: 1px dotted rgba(255,0,0,.9);
}
* * * * * * {
outline-color: gray;
}
*::before, *::after {
background: #faa;
border-radius: 3px;
font: normal normal 400 10px/1.2 monospace;
vertical-align: middle;
padding: 1px 3px;
margin: 0 3px;
}
*::before {
content: "(";
}
*::after {
content: ")";
}
a::before { content: "<a>"; }
a::after { content: "</a>"; }
abbr::before { content: "<abbr>"; }
abbr::after { content: "</abbr>"; }
acronym::before { content: "<acronym>"; }
acronym::after { content: "</acronym>"; }
address::before { content: "<address>"; }
address::after { content: "</address>"; }
applet::before { content: "<applet>"; }
applet::after { content: "</applet>"; }
area::before { content: "<area>"; }
area::after { content: "</area>"; }
article::before { content: "<article>"; }
article::after { content: "</article>"; }
aside::before { content: "<aside>"; }
aside::after { content: "</aside>"; }
audio::before { content: "<audio>"; }
audio::after { content: "</audio>"; }
b::before { content: "<b>"; }
b::after { content: "</b>"; }
base::before { content: "<base>"; }
base::after { content: "</base>"; }
basefont::before { content: "<basefont>"; }
basefont::after { content: "</basefont>"; }
bdi::before { content: "<bdi>"; }
bdi::after { content: "</bdi>"; }
bdo::before { content: "<bdo>"; }
bdo::after { content: "</bdo>"; }
bgsound::before { content: "<bgsound>"; }
bgsound::after { content: "</bgsound>"; }
big::before { content: "<big>"; }
big::after { content: "</big>"; }
blink::before { content: "<blink>"; }
blink::after { content: "</blink>"; }
blockquote::before { content: "<blockquote>"; }
blockquote::after { content: "</blockquote>"; }
body::before { content: "<body>"; }
body::after { content: "</body>"; }
br::before { content: "<br>"; }
br::after { content: "</br>"; }
button::before { content: "<button>"; }
button::after { content: "</button>"; }
caption::before { content: "<caption>"; }
caption::after { content: "</caption>"; }
canvas::before { content: "<canvas>"; }
canvas::after { content: "</canvas>"; }
center::before { content: "<center>"; }
center::after { content: "</center>"; }
cite::before { content: "<cite>"; }
cite::after { content: "</cite>"; }
code::before { content: "<code>"; }
code::after { content: "</code>"; }
col::before { content: "<col>"; }
col::after { content: "</col>"; }
colgroup::before { content: "<colgroup>"; }
colgroup::after { content: "</colgroup>"; }
command::before { content: "<command>"; }
command::after { content: "</command>"; }
content::before { content: "<content>"; }
content::after { content: "</content>"; }
data::before { content: "<data>"; }
data::after { content: "</data>"; }
datalist::before { content: "<datalist>"; }
datalist::after { content: "</datalist>"; }
dd::before { content: "<dd>"; }
dd::after { content: "</dd>"; }
del::before { content: "<del>"; }
del::after { content: "</del>"; }
details::before { content: "<details>"; }
details::after { content: "</details>"; }
dfn::before { content: "<dfn>"; }
dfn::after { content: "</dfn>"; }
dialog::before { content: "<dialog>"; }
dialog::after { content: "</dialog>"; }
dir::before { content: "<dir>"; }
dir::after { content: "</dir>"; }
div::before { content: "<div>"; }
div::after { content: "</div>"; }
dl::before { content: "<dl>"; }
dl::after { content: "</dl>"; }
dt::before { content: "<dt>"; }
dt::after { content: "</dt>"; }
element::before { content: "<element>"; }
element::after { content: "</element>"; }
em::before { content: "<em>"; }
em::after { content: "</em>"; }
embed::before { content: "<embed>"; }
embed::after { content: "</embed>"; }
fieldset::before { content: "<fieldset>"; }
fieldset::after { content: "</fieldset>"; }
figcaption::before { content: "<figcaption>"; }
figcaption::after { content: "</figcaption>"; }
figure::before { content: "<figure>"; }
figure::after { content: "</figure>"; }
font::before { content: "<font>"; }
font::after { content: "</font>"; }
footer::before { content: "<footer>"; }
footer::after { content: "</footer>"; }
form::before { content: "<form>"; }
form::after { content: "</form>"; }
frame::before { content: "<frame>"; }
frame::after { content: "</frame>"; }
frameset::before { content: "<frameset>"; }
frameset::after { content: "</frameset>"; }
h1::before { content: "<h1>"; }
h1::after { content: "</h1>"; }
h2::before { content: "<h2>"; }
h2::after { content: "</h2>"; }
h3::before { content: "<h3>"; }
h3::after { content: "</h3>"; }
h4::before { content: "<h4>"; }
h4::after { content: "</h4>"; }
h5::before { content: "<h5>"; }
h5::after { content: "</h5>"; }
h6::before { content: "<h6>"; }
h6::after { content: "</h6>"; }
head::before { content: "<head>"; }
head::after { content: "</head>"; }
header::before { content: "<header>"; }
header::after { content: "</header>"; }
hgroup::before { content: "<hgroup>"; }
hgroup::after { content: "</hgroup>"; }
hr::before { content: "<hr>"; }
hr::after { content: "</hr>"; }
html::before { content: "<html>"; }
html::after { content: "</html>"; }
i::before { content: "<i>"; }
i::after { content: "</i>"; }
iframe::before { content: "<iframe>"; }
iframe::after { content: "</iframe>"; }
image::before { content: "<image>"; }
image::after { content: "</image>"; }
img::before { content: "<img>"; }
img::after { content: "</img>"; }
input::before { content: "<input>"; }
input::after { content: "</input>"; }
ins::before { content: "<ins>"; }
ins::after { content: "</ins>"; }
isindex::before { content: "<isindex>"; }
isindex::after { content: "</isindex>"; }
kbd::before { content: "<kbd>"; }
kbd::after { content: "</kbd>"; }
keygen::before { content: "<keygen>"; }
keygen::after { content: "</keygen>"; }
label::before { content: "<label>"; }
label::after { content: "</label>"; }
legend::before { content: "<legend>"; }
legend::after { content: "</legend>"; }
li::before { content: "<li>"; }
li::after { content: "</li>"; }
link::before { content: "<link>"; }
link::after { content: "</link>"; }
listing::before { content: "<listing>"; }
listing::after { content: "</listing>"; }
main::before { content: "<main>"; }
main::after { content: "</main>"; }
map::before { content: "<map>"; }
map::after { content: "</map>"; }
mark::before { content: "<mark>"; }
mark::after { content: "</mark>"; }
marquee::before { content: "<marquee>"; }
marquee::after { content: "</marquee>"; }
menu::before { content: "<menu>"; }
menu::after { content: "</menu>"; }
menuitem::before { content: "<menuitem>"; }
menuitem::after { content: "</menuitem>"; }
meta::before { content: "<meta>"; }
meta::after { content: "</meta>"; }
meter::before { content: "<meter>"; }
meter::after { content: "</meter>"; }
multicol::before { content: "<multicol>"; }
multicol::after { content: "</multicol>"; }
nav::before { content: "<nav>"; }
nav::after { content: "</nav>"; }
nextid::before { content: "<nextid>"; }
nextid::after { content: "</nextid>"; }
nobr::before { content: "<nobr>"; }
nobr::after { content: "</nobr>"; }
noembed::before { content: "<noembed>"; }
noembed::after { content: "</noembed>"; }
noframes::before { content: "<noframes>"; }
noframes::after { content: "</noframes>"; }
noscript::before { content: "<noscript>"; }
noscript::after { content: "</noscript>"; }
object::before { content: "<object>"; }
object::after { content: "</object>"; }
ol::before { content: "<ol>"; }
ol::after { content: "</ol>"; }
optgroup::before { content: "<optgroup>"; }
optgroup::after { content: "</optgroup>"; }
option::before { content: "<option>"; }
option::after { content: "</option>"; }
output::before { content: "<output>"; }
output::after { content: "</output>"; }
p::before { content: "<p>"; }
p::after { content: "</p>"; }
param::before { content: "<param>"; }
param::after { content: "</param>"; }
picture::before { content: "<picture>"; }
picture::after { content: "</picture>"; }
plaintext::before { content: "<plaintext>"; }
plaintext::after { content: "</plaintext>"; }
pre::before { content: "<pre>"; }
pre::after { content: "</pre>"; }
progress::before { content: "<progress>"; }
progress::after { content: "</progress>"; }
q::before { content: "<q>"; }
q::after { content: "</q>"; }
rb::before { content: "<rb>"; }
rb::after { content: "</rb>"; }
rp::before { content: "<rp>"; }
rp::after { content: "</rp>"; }
rt::before { content: "<rt>"; }
rt::after { content: "</rt>"; }
rtc::before { content: "<rtc>"; }
rtc::after { content: "</rtc>"; }
ruby::before { content: "<ruby>"; }
ruby::after { content: "</ruby>"; }
s::before { content: "<s>"; }
s::after { content: "</s>"; }
samp::before { content: "<samp>"; }
samp::after { content: "</samp>"; }
script::before { content: "<script>"; }
script::after { content: "</script>"; }
section::before { content: "<section>"; }
section::after { content: "</section>"; }
select::before { content: "<select>"; }
select::after { content: "</select>"; }
shadow::before { content: "<shadow>"; }
shadow::after { content: "</shadow>"; }
slot::before { content: "<slot>"; }
slot::after { content: "</slot>"; }
small::before { content: "<small>"; }
small::after { content: "</small>"; }
source::before { content: "<source>"; }
source::after { content: "</source>"; }
spacer::before { content: "<spacer>"; }
spacer::after { content: "</spacer>"; }
span::before { content: "<span>"; }
span::after { content: "</span>"; }
strike::before { content: "<strike>"; }
strike::after { content: "</strike>"; }
strong::before { content: "<strong>"; }
strong::after { content: "</strong>"; }
style::before { content: "<style>"; }
style::after { content: "<\/style>"; }
sub::before { content: "<sub>"; }
sub::after { content: "</sub>"; }
summary::before { content: "<summary>"; }
summary::after { content: "</summary>"; }
sup::before { content: "<sup>"; }
sup::after { content: "</sup>"; }
table::before { content: "<table>"; }
table::after { content: "</table>"; }
tbody::before { content: "<tbody>"; }
tbody::after { content: "</tbody>"; }
td::before { content: "<td>"; }
td::after { content: "</td>"; }
template::before { content: "<template>"; }
template::after { content: "</template>"; }
textarea::before { content: "<textarea>"; }
textarea::after { content: "</textarea>"; }
tfoot::before { content: "<tfoot>"; }
tfoot::after { content: "</tfoot>"; }
th::before { content: "<th>"; }
th::after { content: "</th>"; }
thead::before { content: "<thead>"; }
thead::after { content: "</thead>"; }
time::before { content: "<time>"; }
time::after { content: "</time>"; }
title::before { content: "<title>"; }
title::after { content: "</title>"; }
tr::before { content: "<tr>"; }
tr::after { content: "</tr>"; }
track::before { content: "<track>"; }
track::after { content: "</track>"; }
tt::before { content: "<tt>"; }
tt::after { content: "</tt>"; }
u::before { content: "<u>"; }
u::after { content: "</u>"; }
ul::before { content: "<ul>"; }
ul::after { content: "</ul>"; }
var::before { content: "<var>"; }
var::after { content: "</var>"; }
video::before { content: "<video>"; }
video::after { content: "</video>"; }
wbr::before { content: "<wbr>"; }
wbr::after { content: "</wbr>"; }
xmp::before { content: "<xmp>"; }
xmp::after { content: "</xmp>"; }

View file

@ -1,22 +1,24 @@
[package]
name = "pagetop-bootsier"
version = "0.0.18"
version = "0.0.20"
edition = "2021"
description = """
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
description = """\
Tema para PageTop que usa Bootstrap para dar vida a tus diseños web.\
"""
categories = ["web-programming", "gui"]
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
categories = ["web-programming", "gui"]
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
repository.workspace = true
homepage.workspace = true
license.workspace = true
authors.workspace = true
homepage = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[dependencies]
pagetop.workspace = true
serde.workspace = true
static-files.workspace = true
[build-dependencies]
pagetop-build.workspace = true

View file

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Manuel Cillero
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Manuel Cillero
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -2,14 +2,13 @@
<h1>PageTop Bootsier</h1>
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
<p>Tema para <strong>PageTop</strong> que usa Bootstrap para dar vida a tus diseños web.</p>
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-license)
[![Doc API](https://img.shields.io/docsrs/pagetop-bootsier?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-bootsier)
[![Crates.io](https://img.shields.io/crates/v/pagetop-bootsier.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-bootsier)
[![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
<br>
</div>
## Sobre PageTop
@ -19,72 +18,11 @@ clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares
configurables, basadas en HTML, CSS y JavaScript.
# ⚡️ Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
```toml
[dependencies]
pagetop-bootsier = "..."
```
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
```rust,no_run
use pagetop::prelude::*;
struct MyApp;
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
// ...
&pagetop_bootsier::Bootsier,
// ...
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
```toml
[app]
theme = "Bootsier"
```
…o **fuerza el tema por código** en una página concreta:
```rust,no_run
use pagetop::prelude::*;
use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Bootsier)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
})),
)
.render()
}
```
# 🚧 Advertencia
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
hasta que se libere la versión **1.0.0**.
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Está en
desarrollo activo, su API es inestable y está sujeta a cambios frecuentes. No recomiendo su uso en
producción, al menos hasta liberar la versión **1.0.0**.
# 📜 Licencia

View file

@ -13,8 +13,7 @@ fn main() -> std::io::Result<()> {
}
fn bootstrap_js_files(path: &Path) -> bool {
let bootstrap_js = "bootstrap.bundle.min.js";
// No filtra durante el desarrollo, solo en la compilación "release".
env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release"
|| path.file_name().is_some_and(|f| f == bootstrap_js)
|| path.file_name().map_or(false, |n| n == "bootstrap.min.js")
}

View file

@ -0,0 +1,143 @@
use pagetop::prelude::*;
use std::fmt;
// Utilities.
mod utility;
pub use utility::*;
// Container.
pub mod container;
pub use container::{Container, ContainerType};
// Grid.
pub mod grid;
pub use grid::Grid;
// Offcanvas.
pub mod offcanvas;
pub use offcanvas::{
Offcanvas, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility,
};
// Image.
mod image;
pub use image::{Image, ImageSize};
// Navbar.
pub mod navbar;
pub use navbar::{Navbar, NavbarContent, NavbarToggler};
// Dropdown.
pub mod dropdown;
pub use dropdown::Dropdown;
/// Define los puntos de interrupción (*breakpoints*) usados por Bootstrap para diseño responsivo.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum BreakPoint {
#[default] // DIMENSIONES - DISPOSITIVOS ---------------------------------------------------
None, // < 576px Muy pequeños: teléfonos en modo vertical, menos de 576px
SM, // >= 576px Pequeños: teléfonos en modo horizontal, 576px o más
MD, // >= 768px Medianos: tabletas, 768px o más
LG, // >= 992px Grandes: puestos de escritorio, 992px o más
XL, // >= 1200px Muy grandes: puestos de escritorio grandes, 1200px o más
XXL, // >= 1400px Extragrandes: puestos de escritorio más grandes, 1400px o más
// ------------------------------------------------------------------------------
Fluid, // Para Container, aplica el 100% del dispositivo siempre
FluidMax(unit::Value) // Para Container, aplica el 100% del dispositivo hasta un ancho máximo
}
impl BreakPoint {
/// Verifica si es un punto de interrupción efectivo en Bootstrap.
///
/// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`. Y `false` en otro caso.
pub fn is_breakpoint(&self) -> bool {
!matches!(
self,
BreakPoint::None | BreakPoint::Fluid | BreakPoint::FluidMax(_)
)
}
/// Genera un nombre de clase CSS basado en el punto de interrupción.
///
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso
/// devuelve únicamente el prefijo.
///
/// # Parámetros
///
/// - `prefix`: Prefijo para concatenar con el punto de interrupción.
///
/// # Ejemplo
///
/// ```rust#ignore
/// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.to_class("col");
/// assert_eq!(class, "col-md".to_string());
///
/// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.to_class("offcanvas");
/// assert_eq!(class, "offcanvas".to_string());
/// ```
pub fn to_class(&self, prefix: impl Into<String>) -> String {
let prefix: String = prefix.into();
if self.is_breakpoint() {
join_string!(prefix, "-", self.to_string())
} else {
prefix
}
}
/// Intenta generar un nombre de clase CSS basado en el punto de interrupción.
///
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso,
/// devuelve `None`.
///
/// # Parámetros
///
/// - `prefix`: Prefijo a concatenar con el punto de interrupción.
///
/// # Retorno
///
/// - `Some(String)`: Si es un punto de interrupción efectivo.
/// - `None`: En otro caso.
///
/// # Ejemplo
///
/// ```rust#ignore
/// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.try_class("col");
/// assert_eq!(class, Some("col-md".to_string()));
///
/// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.try_class("navbar-expanded");
/// assert_eq!(class, None);
/// ```
pub fn try_class(&self, prefix: impl Into<String>) -> Option<String> {
let prefix: String = prefix.into();
if self.is_breakpoint() {
Some(join_string!(prefix, "-", self.to_string()))
} else {
None
}
}
}
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
#[rustfmt::skip]
impl fmt::Display for BreakPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BreakPoint::None => write!(f, ""),
BreakPoint::SM => write!(f, "sm"),
BreakPoint::MD => write!(f, "md"),
BreakPoint::LG => write!(f, "lg"),
BreakPoint::XL => write!(f, "xl"),
BreakPoint::XXL => write!(f, "xxl"),
BreakPoint::Fluid => write!(f, "fluid"),
BreakPoint::FluidMax(_) => write!(f, "fluid"),
}
}
}

View file

@ -0,0 +1,172 @@
use pagetop::prelude::*;
use crate::bs::BreakPoint;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum ContainerType {
#[default]
Default, // Contenedor genérico
Main, // Contenido principal
Header, // Encabezado
Footer, // Pie
Section, // Sección específica de contenido
Article, // Artículo dentro de una sección
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Container {
id : OptionId,
classes : OptionClasses,
container_type: ContainerType,
breakpoint : BreakPoint,
children : Children,
}
impl ComponentTrait for Container {
fn new() -> Self {
Container::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
trio_string!("container", "-", self.breakpoint().to_string()),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let output = self.children().render(cx);
if output.is_empty() {
return PrepareMarkup::None;
}
let style = if let BreakPoint::FluidMax(max_width) = self.breakpoint() {
Some(join_string!("max-width: ", max_width.to_string(), ";"))
} else {
None
};
match self.container_type() {
ContainerType::Default => PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Main => PrepareMarkup::With(html! {
main id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Header => PrepareMarkup::With(html! {
header id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Footer => PrepareMarkup::With(html! {
footer id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Section => PrepareMarkup::With(html! {
section id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
ContainerType::Article => PrepareMarkup::With(html! {
article id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
}
}
}
impl Container {
pub fn main() -> Self {
Container {
container_type: ContainerType::Main,
..Default::default()
}
}
pub fn header() -> Self {
Container {
container_type: ContainerType::Header,
..Default::default()
}
}
pub fn footer() -> Self {
Container {
container_type: ContainerType::Footer,
..Default::default()
}
}
pub fn section() -> Self {
Container {
container_type: ContainerType::Section,
..Default::default()
}
}
pub fn article() -> Self {
Container {
container_type: ContainerType::Article,
..Default::default()
}
}
// Container BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
self.children.add(Child::with(child));
self
}
#[fn_builder]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// Container GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn container_type(&self) -> &ContainerType {
&self.container_type
}
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -0,0 +1,5 @@
mod component;
pub use component::Dropdown;
mod item;
pub use item::Item;

View file

@ -0,0 +1,99 @@
use pagetop::prelude::*;
use crate::bs::dropdown;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Dropdown {
id : OptionId,
classes: OptionClasses,
items : Children,
}
impl ComponentTrait for Dropdown {
fn new() -> Self {
Dropdown::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "dropdown");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
button
type="button"
class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
{
("Dropdown button")
}
ul class="dropdown-menu" {
li {
a class="dropdown-item" href="#" {
("Action")
}
}
li {
a class="dropdown-item" href="#" {
("Another action")
}
}
li {
a class="dropdown-item" href="#" {
("Something else here")
}
}
}
}
})
}
}
impl Dropdown {
// Dropdown BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
pub fn with_item(mut self, item: dropdown::Item) -> Self {
self.items.add(Child::with(item));
self
}
#[fn_builder]
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
self.items.alter_typed(op);
self
}
// Dropdown GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,109 @@
use pagetop::prelude::*;
type Label = L10n;
#[derive(AutoDefault)]
pub enum ItemType {
#[default]
Void,
Label(Label),
Link(Label, FnContextualPath),
LinkBlank(Label, FnContextualPath),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_type: ItemType,
}
impl ComponentTrait 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().path();
match self.item_type() {
ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! {
li class="dropdown-item" {
span title=[description] {
//(left_icon)
(label.escaped(cx.langid()))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if item_path == current_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.escaped(cx.langid()))
//(right_icon)
}
}
})
}
ItemType::LinkBlank(label, path) => {
let item_path = path(cx);
let (class, aria) = if item_path == current_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] target="_blank" {
//(left_icon)
(label.escaped(cx.langid()))
//(right_icon)
}
}
})
}
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_type: ItemType::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnContextualPath) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnContextualPath) -> 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,110 @@
use pagetop::prelude::*;
use crate::bs::BreakPoint;
use std::fmt;
mod component;
pub use component::Grid;
mod item;
pub use item::Item;
#[derive(AutoDefault)]
pub enum Layout {
#[default]
Default,
Rows(u8),
Cols(u8),
Grid(u8, u8),
}
#[rustfmt::skip]
impl fmt::Display for Layout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Layout::Rows(r) if *r > 1 => write!(f, "--bs-rows: {r};"),
Layout::Cols(c) if *c > 0 => write!(f, "--bs-columns: {c};"),
Layout::Grid(r, c) => write!(f, "{}", trio_string!(
Layout::Rows(*r).to_string(), " ", Layout::Cols(*c).to_string()
)),
_ => write!(f, ""),
}
}
}
#[derive(AutoDefault)]
pub enum Gap {
#[default]
Default,
Row(unit::Value),
Col(unit::Value),
Grid(unit::Value, unit::Value),
Both(unit::Value),
}
#[rustfmt::skip]
impl fmt::Display for Gap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Gap::Default => write!(f, ""),
Gap::Row(r) => write!(f, "row-gap: {r};"),
Gap::Col(c) => write!(f, "column-gap: {c};"),
Gap::Grid(r, c) => write!(f, "--bs-gap: {r} {c};"),
Gap::Both(v) => write!(f, "--bs-gap: {v};"),
}
}
}
#[derive(AutoDefault)]
pub enum ItemColumns {
#[default]
Default,
Cols(u8),
}
#[rustfmt::skip]
impl fmt::Display for ItemColumns {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ItemColumns::Cols(c) if *c > 1 => write!(f, "g-col-{c}"),
_ => write!(f, ""),
}
}
}
#[derive(AutoDefault)]
pub enum ItemResponsive {
#[default]
Default,
Cols(BreakPoint, u8),
}
#[rustfmt::skip]
impl fmt::Display for ItemResponsive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ItemResponsive::Cols(bp, c) if bp.is_breakpoint() && *c > 0 => {
write!(f, "g-col-{bp}-{c}")
}
_ => write!(f, ""),
}
}
}
#[derive(AutoDefault)]
pub enum ItemStart {
#[default]
Default,
Col(u8),
}
#[rustfmt::skip]
impl fmt::Display for ItemStart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ItemStart::Col(c) if *c > 1 => write!(f, "g-start-{c}"),
_ => write!(f, ""),
}
}
}

View file

@ -0,0 +1,103 @@
use pagetop::prelude::*;
use crate::bs::grid;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Grid {
id : OptionId,
classes : OptionClasses,
grid_layout: grid::Layout,
grid_gap : grid::Gap,
items : Children,
}
impl ComponentTrait for Grid {
fn new() -> Self {
Grid::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, "grid");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let output = self.items().render(cx);
if output.is_empty() {
return PrepareMarkup::None;
}
let style = option_string!([self.layout().to_string(), self.gap().to_string()]; " ");
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
})
}
}
impl Grid {
pub fn with(item: grid::Item) -> Self {
Grid::default().with_item(item)
}
// Grid BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_layout(mut self, layout: grid::Layout) -> Self {
self.grid_layout = layout;
self
}
#[fn_builder]
pub fn with_gap(mut self, gap: grid::Gap) -> Self {
self.grid_gap = gap;
self
}
pub fn with_item(mut self, item: grid::Item) -> Self {
self.items.add(Child::with(item));
self
}
#[fn_builder]
pub fn with_items(mut self, op: TypedOp<grid::Item>) -> Self {
self.items.alter_typed(op);
self
}
// Grid GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn layout(&self) -> &grid::Layout {
&self.grid_layout
}
pub fn gap(&self) -> &grid::Gap {
&self.grid_gap
}
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,119 @@
use pagetop::prelude::*;
use crate::bs::grid;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
id : OptionId,
classes : OptionClasses,
columns : grid::ItemColumns,
responsive: grid::ItemResponsive,
start : grid::ItemStart,
children : Children,
}
impl ComponentTrait for Item {
fn new() -> Self {
Item::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[
self.columns().to_string(),
self.responsive().to_string(),
self.start().to_string(),
]
.join(" "),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let output = self.children().render(cx);
if output.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
(output)
}
})
}
}
impl Item {
pub fn with(child: impl ComponentTrait) -> Self {
Item::default().with_child(child)
}
// Item BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_columns(mut self, columns: grid::ItemColumns) -> Self {
self.columns = columns;
self
}
#[fn_builder]
pub fn with_responsive(mut self, responsive: grid::ItemResponsive) -> Self {
self.responsive = responsive;
self
}
#[fn_builder]
pub fn with_start(mut self, start: grid::ItemStart) -> Self {
self.start = start;
self
}
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
self.children.add(Child::with(child));
self
}
#[fn_builder]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// Item GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn columns(&self) -> &grid::ItemColumns {
&self.columns
}
pub fn responsive(&self) -> &grid::ItemResponsive {
&self.responsive
}
pub fn start(&self) -> &grid::ItemStart {
&self.start
}
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -0,0 +1,157 @@
use pagetop::prelude::*;
use crate::bs::Border;
use std::fmt;
#[derive(AutoDefault)]
pub enum ImageType {
#[default]
Fluid,
Thumbnail,
}
#[rustfmt::skip]
impl fmt::Display for ImageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImageType::Fluid => write!(f, "img-fluid"),
ImageType::Thumbnail => write!(f, "img-thumbnail"),
}
}
}
#[derive(AutoDefault)]
pub enum ImageSize {
#[default]
Auto,
Size(u16, u16),
Width(u16),
Height(u16),
Both(u16),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Image {
id : OptionId,
classes : OptionClasses,
image_type: ImageType,
source : OptionString,
size : ImageSize,
border : Border,
}
impl ComponentTrait for Image {
fn new() -> Self {
Image::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
[self.image_type().to_string()].join(" "),
);
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
let (width, height) = match self.size() {
ImageSize::Auto => (None, None),
ImageSize::Size(width, height) => (Some(width), Some(height)),
ImageSize::Width(width) => (Some(width), None),
ImageSize::Height(height) => (None, Some(height)),
ImageSize::Both(value) => (Some(value), Some(value)),
};
PrepareMarkup::With(html! {
img
src=[self.source().get()]
id=[self.id()]
class=[self.classes().get()]
width=[width]
height=[height] {}
})
}
}
impl Image {
pub fn with(source: &str) -> Self {
Image::default().with_source(source)
}
pub fn thumbnail(source: &str) -> Self {
Image::default()
.with_source(source)
.with_image_type(ImageType::Thumbnail)
}
/*
pub fn pagetop() -> Self {
Image::default()
.with_source("/base/pagetop-logo.svg")
.with_classes(ClassesOp::Add, IMG_FIXED)
.with_size(ImageSize::Size(64, 64))
}
*/
// Image BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_image_type(mut self, image_type: ImageType) -> Self {
self.image_type = image_type;
self
}
#[fn_builder]
pub fn with_source(mut self, source: &str) -> Self {
self.source.alter_value(source);
self
}
#[fn_builder]
pub fn with_size(mut self, size: ImageSize) -> Self {
self.size = size;
self
}
#[fn_builder]
pub fn with_border(mut self, border: Border) -> Self {
self.border = border;
self
}
// Image GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn image_type(&self) -> &ImageType {
&self.image_type
}
pub fn source(&self) -> &OptionString {
&self.source
}
pub fn size(&self) -> &ImageSize {
&self.size
}
pub fn border(&self) -> &Border {
&self.border
}
}

View file

@ -0,0 +1,11 @@
mod component;
pub use component::{Navbar, NavbarContent, NavbarToggler};
//mod brand;
//pub use brand::Brand;
mod nav;
pub use nav::Nav;
mod item;
pub use item::{Item, ItemType};

View file

@ -0,0 +1,102 @@
use pagetop::prelude::*;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Brand {
id : OptionId,
#[default(_code = "global::SETTINGS.app.name.to_owned()")]
app_name : String,
slogan : OptionTranslated,
logo : OptionComponent<Image>,
#[default(_code = "|_| \"/\"")]
home : FnContextualPath,
}
impl ComponentTrait 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").using(cx.langid());
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().using(cx.langid()) {
div class="branding__slogan" {
(slogan)
}
}
}
}
}
})
}
}
impl Brand {
// Brand BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
self.app_name = app_name.into();
self
}
#[fn_builder]
pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan.alter_value(slogan);
self
}
#[fn_builder]
pub fn with_logo(mut self, logo: Option<Image>) -> Self {
self.logo.alter_value(logo);
self
}
#[fn_builder]
pub fn with_home(mut self, home: FnContextualPath) -> Self {
self.home = home;
self
}
// Brand GETTERS.
pub fn app_name(&self) -> &String {
&self.app_name
}
pub fn slogan(&self) -> &OptionTranslated {
&self.slogan
}
pub fn logo(&self) -> &OptionComponent<Image> {
&self.logo
}
pub fn home(&self) -> &FnContextualPath {
&self.home
}
}

View file

@ -0,0 +1,202 @@
use pagetop::prelude::*;
use crate::bs::navbar;
use crate::bs::{BreakPoint, Offcanvas};
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 NavbarContent {
#[default]
None,
Nav(Typed<navbar::Nav>),
Offcanvas(Typed<Offcanvas>),
Text(L10n),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Navbar {
id : OptionId,
classes: OptionClasses,
expand : BreakPoint,
toggler: NavbarToggler,
content: NavbarContent,
}
impl ComponentTrait 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 content = match self.content() {
NavbarContent::None => return PrepareMarkup::None,
NavbarContent::Nav(nav) => {
let id_content = join_string!(id, "-content");
match self.toggler() {
NavbarToggler::Enabled => self.toggler_wrapper(
TOGGLE_COLLAPSE,
L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
id_content,
nav.render(cx),
),
NavbarToggler::Disabled => nav.render(cx),
}
}
NavbarContent::Offcanvas(oc) => {
let id_content = oc.id().unwrap_or_default();
self.toggler_wrapper(
TOGGLE_OFFCANVAS,
L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
id_content,
oc.render(cx),
)
}
NavbarContent::Text(text) => html! {
span class="navbar-text" {
(text.escaped(cx.langid()))
}
},
};
self.nav_wrapper(id, content)
}
}
impl Navbar {
pub fn with_nav(nav: navbar::Nav) -> Self {
Navbar::default().with_content(NavbarContent::Nav(Typed::with(nav)))
}
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
Navbar::default().with_content(NavbarContent::Offcanvas(Typed::with(offcanvas)))
}
// Navbar BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp;
self
}
#[fn_builder]
pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
self.toggler = toggler;
self
}
#[fn_builder]
pub fn with_content(mut self, content: NavbarContent) -> Self {
self.content = content;
self
}
// Navbar GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn expand(&self) -> &BreakPoint {
&self.expand
}
pub fn toggler(&self) -> &NavbarToggler {
&self.toggler
}
pub fn content(&self) -> &NavbarContent {
&self.content
}
// Navbar HELPERS.
fn nav_wrapper(&self, id: String, content: Markup) -> PrepareMarkup {
if content.is_empty() {
PrepareMarkup::None
} else {
PrepareMarkup::With(html! {
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,
content: Markup,
) -> Markup {
if content.is_empty() {
html! {}
} else {
let id_content_target = join_string!("#", 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=[aria_label]
{
span class="navbar-toggler-icon" {}
}
div id=(id_content) class="collapse navbar-collapse" {
(content)
}
}
}
}
}

View file

@ -0,0 +1,113 @@
use pagetop::prelude::*;
use crate::bs::Dropdown;
type Label = L10n;
#[derive(AutoDefault)]
pub enum ItemType {
#[default]
Void,
Label(Label),
Link(Label, FnContextualPath),
LinkBlank(Label, FnContextualPath),
Dropdown(Typed<Dropdown>),
}
// Item.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
item_type: ItemType,
}
impl ComponentTrait 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().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.escaped(cx.langid()))
//(right_icon)
}
}
}),
ItemType::Link(label, path) => {
let item_path = path(cx);
let (class, aria) = if item_path == current_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.escaped(cx.langid()))
//(right_icon)
}
}
})
}
ItemType::LinkBlank(label, path) => {
let item_path = path(cx);
let (class, aria) = if item_path == current_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.escaped(cx.langid()))
//(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: FnContextualPath) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnContextualPath) -> 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::bs::navbar;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Nav {
id : OptionId,
classes: OptionClasses,
items : Children,
}
impl ComponentTrait 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.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> 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
}
#[fn_builder]
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
self.items.alter_typed(op);
self
}
// Nav GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -0,0 +1,59 @@
use pagetop::prelude::*;
use std::fmt;
mod component;
pub use component::Offcanvas;
#[derive(AutoDefault)]
pub enum OffcanvasPlacement {
#[default]
Start,
End,
Top,
Bottom,
}
#[rustfmt::skip]
impl fmt::Display for OffcanvasPlacement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasPlacement::Start => write!(f, "offcanvas-start"),
OffcanvasPlacement::End => write!(f, "offcanvas-end"),
OffcanvasPlacement::Top => write!(f, "offcanvas-top"),
OffcanvasPlacement::Bottom => write!(f, "offcanvas-bottom"),
}
}
}
#[derive(AutoDefault)]
pub enum OffcanvasVisibility {
#[default]
Default,
Show,
}
#[rustfmt::skip]
impl fmt::Display for OffcanvasVisibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OffcanvasVisibility::Default => write!(f, "show"),
OffcanvasVisibility::Show => write!(f, ""),
}
}
}
#[derive(AutoDefault)]
pub enum OffcanvasBodyScroll {
#[default]
Disabled,
Enabled,
}
#[derive(AutoDefault)]
pub enum OffcanvasBackdrop {
Disabled,
#[default]
Enabled,
Static,
}

View file

@ -0,0 +1,187 @@
use pagetop::prelude::*;
use crate::bs::BreakPoint;
use crate::bs::{OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility};
use crate::LOCALES_BOOTSIER;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Offcanvas {
id : OptionId,
classes : OptionClasses,
title : OptionTranslated,
breakpoint: BreakPoint,
placement : OffcanvasPlacement,
visibility: OffcanvasVisibility,
scrolling : OffcanvasBodyScroll,
backdrop : OffcanvasBackdrop,
children : Children,
}
impl ComponentTrait 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_string!(id, "-label");
let id_target = join_string!("#", id);
let body_scroll = match self.body_scroll() {
OffcanvasBodyScroll::Disabled => None,
OffcanvasBodyScroll::Enabled => Some("true".to_string()),
};
let backdrop = match self.backdrop() {
OffcanvasBackdrop::Disabled => Some("true".to_string()),
OffcanvasBackdrop::Enabled => None,
OffcanvasBackdrop::Static => Some("static".to_string()),
};
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" {
h5 class="offcanvas-title" id=(id_label) {
(self.title().escaped(cx.langid()))
}
button
type="button"
class="btn-close"
data-bs-dismiss="offcanvas"
data-bs-target=(id_target)
aria-label=[L10n::t("close", &LOCALES_BOOTSIER).using(cx.langid())]
{}
}
div class="offcanvas-body" {
(body)
}
}
})
}
}
impl Offcanvas {
// Offcanvas BUILDER.
#[fn_builder]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id.alter_value(id);
self
}
#[fn_builder]
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.classes.alter_value(op, classes);
self
}
#[fn_builder]
pub fn with_title(mut self, title: L10n) -> Self {
self.title.alter_value(title);
self
}
#[fn_builder]
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
self.breakpoint = bp;
self
}
#[fn_builder]
pub fn with_placement(mut self, placement: OffcanvasPlacement) -> Self {
self.placement = placement;
self
}
#[fn_builder]
pub fn with_visibility(mut self, visibility: OffcanvasVisibility) -> Self {
self.visibility = visibility;
self
}
#[fn_builder]
pub fn with_body_scroll(mut self, scrolling: OffcanvasBodyScroll) -> Self {
self.scrolling = scrolling;
self
}
#[fn_builder]
pub fn with_backdrop(mut self, backdrop: OffcanvasBackdrop) -> Self {
self.backdrop = backdrop;
self
}
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
self.children.add(Child::with(child));
self
}
#[fn_builder]
pub fn with_children(mut self, op: ChildOp) -> Self {
self.children.alter_child(op);
self
}
// Offcanvas GETTERS.
pub fn classes(&self) -> &OptionClasses {
&self.classes
}
pub fn title(&self) -> &OptionTranslated {
&self.title
}
pub fn breakpoint(&self) -> &BreakPoint {
&self.breakpoint
}
pub fn placement(&self) -> &OffcanvasPlacement {
&self.placement
}
pub fn visibility(&self) -> &OffcanvasVisibility {
&self.visibility
}
pub fn body_scroll(&self) -> &OffcanvasBodyScroll {
&self.scrolling
}
pub fn backdrop(&self) -> &OffcanvasBackdrop {
&self.backdrop
}
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -0,0 +1,10 @@
mod color;
pub use color::Color;
pub use color::{BgColor, BorderColor, TextColor};
mod opacity;
pub use opacity::Opacity;
pub use opacity::{BgOpacity, BorderOpacity, TextOpacity};
mod border;
pub use border::{Border, BorderSize};

View file

@ -0,0 +1,102 @@
use pagetop::{prelude::*, strict_string};
use crate::bs::{BorderColor, BorderOpacity};
use std::fmt;
#[derive(AutoDefault)]
pub enum BorderSize {
#[default]
Default,
Zero,
Width1,
Width2,
Width3,
Width4,
Width5,
}
#[rustfmt::skip]
impl fmt::Display for BorderSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BorderSize::Default => write!(f, ""),
BorderSize::Zero => write!(f, "0"),
BorderSize::Width1 => write!(f, "1"),
BorderSize::Width2 => write!(f, "2"),
BorderSize::Width3 => write!(f, "3"),
BorderSize::Width4 => write!(f, "4"),
BorderSize::Width5 => write!(f, "5"),
}
}
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Border {
color : BorderColor,
opacity: BorderOpacity,
size : BorderSize,
top : BorderSize,
end : BorderSize,
bottom : BorderSize,
start : BorderSize,
}
impl Border {
pub fn new() -> Self {
Self::default()
}
pub fn with(size: BorderSize) -> Self {
Self::default().with_size(size)
}
// Border BUILDER.
pub fn with_color(mut self, color: BorderColor) -> Self {
self.color = color;
self
}
pub fn with_opacity(mut self, opacity: BorderOpacity) -> Self {
self.opacity = opacity;
self
}
pub fn with_size(mut self, size: BorderSize) -> Self {
self.size = size;
self
}
pub fn with_top(mut self, size: BorderSize) -> Self {
self.top = size;
self
}
pub fn with_end(mut self, size: BorderSize) -> Self {
self.end = size;
self
}
pub fn with_bottom(mut self, size: BorderSize) -> Self {
self.bottom = size;
self
}
pub fn with_start(mut self, size: BorderSize) -> Self {
self.start = size;
self
}
}
#[rustfmt::skip]
impl fmt::Display for Border {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", strict_string!([
"border",
&self.color.to_string(),
&self.opacity.to_string(),
]; " ").unwrap_or_default())
}
}

View file

@ -0,0 +1,119 @@
use pagetop::prelude::*;
use std::fmt;
#[derive(AutoDefault)]
pub enum Color {
#[default]
Primary,
Secondary,
Success,
Info,
Warning,
Danger,
Light,
Dark,
}
#[rustfmt::skip]
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Primary => write!(f, "primary"),
Color::Secondary => write!(f, "secondary"),
Color::Success => write!(f, "success"),
Color::Info => write!(f, "info"),
Color::Warning => write!(f, "warning"),
Color::Danger => write!(f, "danger"),
Color::Light => write!(f, "light"),
Color::Dark => write!(f, "dark"),
}
}
}
#[derive(AutoDefault)]
pub enum BgColor {
#[default]
Default,
Body,
BodySecondary,
BodyTertiary,
Theme(Color),
Subtle(Color),
Black,
White,
Transparent,
}
#[rustfmt::skip]
impl fmt::Display for BgColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BgColor::Default => write!(f, ""),
BgColor::Body => write!(f, "bg-body"),
BgColor::BodySecondary => write!(f, "bg-body-secondary"),
BgColor::BodyTertiary => write!(f, "bg-body-tertiary"),
BgColor::Theme(c) => write!(f, "bg-{}", c),
BgColor::Subtle(c) => write!(f, "bg-{}-subtle", c),
BgColor::Black => write!(f, "bg-black"),
BgColor::White => write!(f, "bg-white"),
BgColor::Transparent => write!(f, "bg-transparent"),
}
}
}
#[derive(AutoDefault)]
pub enum BorderColor {
#[default]
Default,
Theme(Color),
Subtle(Color),
Black,
White,
}
#[rustfmt::skip]
impl fmt::Display for BorderColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BorderColor::Default => write!(f, ""),
BorderColor::Theme(c) => write!(f, "border-{}", c),
BorderColor::Subtle(c) => write!(f, "border-{}-subtle", c),
BorderColor::Black => write!(f, "border-black"),
BorderColor::White => write!(f, "border-white"),
}
}
}
#[derive(AutoDefault)]
pub enum TextColor {
#[default]
Default,
Body,
BodyEmphasis,
BodySecondary,
BodyTertiary,
Theme(Color),
Emphasis(Color),
Background(Color),
Black,
White,
}
#[rustfmt::skip]
impl fmt::Display for TextColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TextColor::Default => write!(f, ""),
TextColor::Body => write!(f, "text-body"),
TextColor::BodyEmphasis => write!(f, "text-body-emphasis"),
TextColor::BodySecondary => write!(f, "text-body-secondary"),
TextColor::BodyTertiary => write!(f, "text-body-tertiary"),
TextColor::Theme(c) => write!(f, "text-{}", c),
TextColor::Emphasis(c) => write!(f, "text-{}-emphasis", c),
TextColor::Background(c) => write!(f, "text-bg-{}", c),
TextColor::Black => write!(f, "text-black"),
TextColor::White => write!(f, "text-white"),
}
}
}

View file

@ -0,0 +1,78 @@
use pagetop::prelude::*;
use std::fmt;
#[rustfmt::skip]
#[derive(AutoDefault)]
pub enum Opacity {
#[default]
Opaque, // 100%
SemiOpaque, // 75%
Half, // 50%
SemiTransparent, // 25%
AlmostTransparent, // 10%
}
#[rustfmt::skip]
impl fmt::Display for Opacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Opacity::Opaque => write!(f, "opacity-100"),
Opacity::SemiOpaque => write!(f, "opacity-75"),
Opacity::Half => write!(f, "opacity-50"),
Opacity::SemiTransparent => write!(f, "opacity-25"),
Opacity::AlmostTransparent => write!(f, "opacity-10"),
}
}
}
#[derive(AutoDefault)]
pub enum BgOpacity {
#[default]
Default,
Theme(Opacity),
}
#[rustfmt::skip]
impl fmt::Display for BgOpacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BgOpacity::Default => write!(f, ""),
BgOpacity::Theme(o) => write!(f, "bg-{}", o),
}
}
}
#[derive(AutoDefault)]
pub enum BorderOpacity {
#[default]
Default,
Theme(Opacity),
}
#[rustfmt::skip]
impl fmt::Display for BorderOpacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BorderOpacity::Default => write!(f, ""),
BorderOpacity::Theme(o) => write!(f, "border-{}", o),
}
}
}
#[derive(AutoDefault)]
pub enum TextOpacity {
#[default]
Default,
Theme(Opacity),
}
#[rustfmt::skip]
impl fmt::Display for TextOpacity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TextOpacity::Default => write!(f, ""),
TextOpacity::Theme(o) => write!(f, "text-{}", o),
}
}
}

View file

@ -0,0 +1,116 @@
use pagetop::prelude::*;
use crate::bs::{Color, Opacity};
#[derive(AutoDefault)]
pub enum BorderSize {
#[default]
None,
Width1,
Width2,
Width3,
Width4,
Width5,
Free(unit::Value),
}
#[derive(AutoDefault)]
pub enum BorderRadius {
#[default]
None,
Rounded1,
Rounded2,
Rounded3,
Rounded4,
Rounded5,
Circle,
Pill,
Free(f32),
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct BorderProperty {
color : Color,
opacity: Opacity,
size : BorderSize,
radius : BorderRadius,
}
impl BorderProperty {
pub fn new() -> Self {
BorderProperty::default()
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
pub fn with_size(mut self, size: BorderSize) -> Self {
self.size = size;
self
}
pub fn with_radius(mut self, radius: BorderRadius) -> Self {
self.radius = radius;
self
}
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Border {
all : Option<BorderProperty>,
top : Option<BorderProperty>,
end : Option<BorderProperty>,
bottom: Option<BorderProperty>,
start : Option<BorderProperty>,
}
impl Border {
pub fn new() -> Self {
Self::default()
}
// Border BUILDER.
pub fn with_all(mut self, border: BorderProperty) -> Self {
self.all = Some(border);
self
}
pub fn with_top(mut self, border: BorderProperty) -> Self {
self.top = Some(border);
self
}
pub fn with_end(mut self, border: BorderProperty) -> Self {
self.end = Some(border);
self
}
pub fn with_bottom(mut self, border: BorderProperty) -> Self {
self.bottom = Some(border);
self
}
pub fn with_start(mut self, border: BorderProperty) -> Self {
self.start = Some(border);
self
}
pub fn with_none(mut self) -> Self {
self.all = None;
self.top = None;
self.end = None;
self.bottom = None;
self.start = None;
self
}
}

View file

@ -1,4 +1,4 @@
//! Opciones de configuración del tema.
//! Opciones de configuración.
//!
//! Ejemplo:
//!
@ -9,15 +9,14 @@
//!
//! Uso:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! ```rust#ignore
//! use pagetop_bootsier::config;
//!
//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440));
//! assert_eq!(config::SETTINGS.bootsier.max_width, unit::Value::Rem(90));
//! ```
//!
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
//! los valores a los ajustes.
//! Consulta [`pagetop::config`] para aprender cómo `PageTop` lee los archivos de opciones y aplica
//! los valores de configuración.
use pagetop::prelude::*;
@ -29,13 +28,16 @@ include_config!(SETTINGS: Settings => [
]);
#[derive(Debug, Deserialize)]
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
/// Opciones de configuración para la sección [`[bootsier]`](Bootsier) (ver [`SETTINGS`]).
pub struct Settings {
pub bootsier: Bootsier,
}
#[derive(Debug, Deserialize)]
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
/// Sección `[bootsier]` de la configuración.
///
/// Ver [`Settings`].
pub struct Bootsier {
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
pub max_width: UnitValue,
/// Valor por defecto: *"1440px"*
pub max_width: unit::Value,
}

View file

@ -1,132 +1,221 @@
/*!
<div align="center">
<h1>PageTop Bootsier</h1>
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
[![Doc API](https://img.shields.io/docsrs/pagetop-bootsier?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-bootsier)
[![Crates.io](https://img.shields.io/crates/v/pagetop-bootsier.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-bootsier)
[![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier)
[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
<br>
</div>
## Sobre PageTop
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
configurables, basadas en HTML, CSS y JavaScript.
# Guía rápida
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
```toml
[dependencies]
pagetop-bootsier = "..."
```
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
```rust,no_run
use pagetop::prelude::*;
struct MyApp;
// GLOBAL ******************************************************************************************
impl Extension for MyApp {
fn dependencies(&self) -> Vec<ExtensionRef> {
vec![
// ...
&pagetop_bootsier::Bootsier,
// ...
]
}
}
#[pagetop::main]
async fn main() -> std::io::Result<()> {
Application::prepare(&MyApp).run()?.await
}
```
Y **selecciona el tema en la configuración** de la aplicación:
```toml
[app]
theme = "Bootsier"
```
o **fuerza el tema por código** en una página concreta:
```rust,no_run
use pagetop::prelude::*;
use pagetop_bootsier::Bootsier;
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
Page::new(request)
.with_theme(&Bootsier)
.add_child(
Block::new()
.with_title(L10n::l("sample_title"))
.add_child(Html::with(|cx| html! {
p { (L10n::l("sample_content").using(cx)) }
})),
)
.render()
}
```
*/
#![doc(
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
)]
use pagetop::prelude::*;
include_files!(bootsier_bs);
include_files!(bootsier_js);
include_locales!(LOCALES_BOOTSIER);
// Versión de la librería Bootstrap.
const BOOTSTRAP_VERSION: &str = "5.3.8";
const BOOTSTRAP_VERSION: &str = "5.3.3"; // Versión de la librería Bootstrap.
// API *********************************************************************************************
pub mod config;
pub mod theme;
pub mod bs;
/// *Prelude* del tema.
pub mod prelude {
pub use crate::config::*;
pub use crate::theme::*;
}
/// Implementa el tema.
pub struct Bootsier;
impl Extension for Bootsier {
impl ExtensionTrait for Bootsier {
fn theme(&self) -> Option<ThemeRef> {
Some(&Self)
Some(&Bootsier)
}
fn actions(&self) -> Vec<ActionBox> {
actions![
//action::theme::BeforeRender::<Region>::new(&Self, before_render_region),
//action::theme::BeforePrepare::<Button>::new(&Self, before_prepare_button),
//action::theme::BeforePrepare::<Heading>::new(&Self, before_prepare_heading),
//action::theme::BeforePrepare::<Paragraph>::new(&Self, before_prepare_paragraph),
//action::theme::RenderComponent::<Error404>::new(&Self, render_error404),
]
}
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
include_files_service!(scfg, bootsier_bs => "/bootsier/bs");
include_files_service!(scfg, bootsier_js => "/bootsier/js");
}
}
impl Theme for Bootsier {
impl ThemeTrait for Bootsier {
#[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![
("region-header", L10n::t("header", &LOCALES_BOOTSIER)),
("region-nav_branding", L10n::t("nav_branding", &LOCALES_BOOTSIER)),
("region-nav_main", L10n::t("nav_main", &LOCALES_BOOTSIER)),
("region-nav_additional", L10n::t("nav_additional", &LOCALES_BOOTSIER)),
("region-breadcrumb", L10n::t("breadcrumb", &LOCALES_BOOTSIER)),
("region-content", L10n::t("content", &LOCALES_BOOTSIER)),
("region-sidebar_first", L10n::t("sidebar_first", &LOCALES_BOOTSIER)),
("region-sidebar_second", L10n::t("sidebar_second", &LOCALES_BOOTSIER)),
("region-footer", L10n::t("footer", &LOCALES_BOOTSIER)),
]
}
fn render_page_body(&self, page: &mut Page) -> Markup {
html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
//@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
// div class="skip__to_content" {
// a href=(concat_string!("#", skip_to_id)) { (skip) }
// }
//}
(bs::Container::new()
.with_id("container-wrapper")
.with_breakpoint(bs::BreakPoint::FluidMax(config::SETTINGS.bootsier.max_width))
.with_child(Region::of("region-content"))
.render(page.context()))
}
}
}
fn after_render_page_body(&self, page: &mut Page) {
page.alter_assets(ContextOp::AddStyleSheet(
page.alter_assets(AssetsOp::AddStyleSheet(
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
.with_version(BOOTSTRAP_VERSION)
.with_weight(-90),
.with_weight(-99),
))
.alter_assets(ContextOp::AddJavaScript(
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
.alter_assets(AssetsOp::AddJavaScript(
JavaScript::defer("/bootsier/js/bootstrap.min.js")
.with_version(BOOTSTRAP_VERSION)
.with_weight(-90),
.with_weight(-99),
));
}
/*
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
div class="skip__to_content" {
a href=(concat_string!("#", skip_to_id)) { (skip) }
}
}
(match page.context().layout() {
"admin" => flex::Container::new()
.add_item(flex::Item::region().with_id("top-menu"))
.add_item(flex::Item::region().with_id("side-menu"))
.add_item(flex::Item::region().with_id("content")),
_ => flex::Container::new()
.add_item(flex::Item::region().with_id("header"))
.add_item(flex::Item::region().with_id("nav_branding"))
.add_item(flex::Item::region().with_id("nav_main"))
.add_item(flex::Item::region().with_id("nav_additional"))
.add_item(flex::Item::region().with_id("breadcrumb"))
.add_item(flex::Item::region().with_id("content"))
.add_item(flex::Item::region().with_id("sidebar_first"))
.add_item(flex::Item::region().with_id("sidebar_second"))
.add_item(flex::Item::region().with_id("footer")),
}.render(page.context()))
}
})
}
*/
/*
}
fn before_prepare_icon(i: &mut Icon, _cx: &mut Context) {
i.set_classes(
ClassesOp::Replace(i.font_size().to_string()),
with_font(i.font_size()),
);
}
#[rustfmt::skip]
fn before_prepare_button(b: &mut Button, _cx: &mut Context) {
b.set_classes(ClassesOp::Replace("button__tap".to_owned()), "btn");
b.set_classes(
ClassesOp::Replace(b.style().to_string()),
match b.style() {
StyleBase::Default => "btn-primary",
StyleBase::Info => "btn-info",
StyleBase::Success => "btn-success",
StyleBase::Warning => "btn-warning",
StyleBase::Danger => "btn-danger",
StyleBase::Light => "btn-light",
StyleBase::Dark => "btn-dark",
StyleBase::Link => "btn-link",
},
);
b.set_classes(
ClassesOp::Replace(b.font_size().to_string()),
with_font(b.font_size()),
);
}
#[rustfmt::skip]
fn before_prepare_heading(h: &mut Heading, _cx: &mut Context) {
h.set_classes(
ClassesOp::Replace(h.size().to_string()),
match h.size() {
HeadingSize::ExtraLarge => "display-1",
HeadingSize::XxLarge => "display-2",
HeadingSize::XLarge => "display-3",
HeadingSize::Large => "display-4",
HeadingSize::Medium => "display-5",
_ => "",
},
);
}
fn before_prepare_paragraph(p: &mut Paragraph, _cx: &mut Context) {
p.set_classes(
ClassesOp::Replace(p.font_size().to_string()),
with_font(p.font_size()),
);
}
fn render_error404(_: &Error404, cx: &mut Context) -> Option<Markup> {
Some(html! {
div class="jumbotron" {
div class="media" {
img
src="/bootsier/images/caution.png"
class="mr-4"
style="width: 20%; max-width: 188px"
alt="Caution!";
div class="media-body" {
h1 class="display-4" { ("RESOURCE NOT FOUND") }
p class="lead" {
(L10n::t("e404-description", &LOCALES_BOOTSIER)
.escaped(cx.langid()))
}
hr class="my-4";
p {
(L10n::t("e404-description", &LOCALES_BOOTSIER)
.escaped(cx.langid()))
}
a
class="btn btn-primary btn-lg"
href="/"
role="button"
{
(L10n::t("back-homepage", &LOCALES_BOOTSIER)
.escaped(cx.langid()))
}
}
}
}
})
*/
}
/*
#[rustfmt::skip]
fn with_font(font_size: &FontSize) -> String {
String::from(match font_size {
FontSize::ExtraLarge => "fs-1",
FontSize::XxLarge => "fs-2",
FontSize::XLarge => "fs-3",
FontSize::Large => "fs-4",
FontSize::Medium => "fs-5",
_ => "",
})
}
*/

View file

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

View file

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

View file

@ -1,40 +0,0 @@
//! Definiciones y componentes del tema.
//!
//! En esta página, el apartado **Modules** incluye las definiciones necesarias para los componentes
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
//! auxiliares del tema utilizados en clases y componentes.
mod aux;
pub use aux::*;
pub mod classes;
// Container.
pub mod container;
#[doc(inline)]
pub use container::Container;
// Dropdown.
pub mod dropdown;
#[doc(inline)]
pub use dropdown::Dropdown;
// Image.
pub mod image;
#[doc(inline)]
pub use image::Image;
// Nav.
pub mod nav;
#[doc(inline)]
pub use nav::Nav;
// Navbar.
pub mod navbar;
#[doc(inline)]
pub use navbar::Navbar;
// Offcanvas.
pub mod offcanvas;
#[doc(inline)]
pub use offcanvas::Offcanvas;

View file

@ -1,20 +0,0 @@
//! Colección de elementos auxiliares de Bootstrap para Bootsier.
mod breakpoint;
pub use breakpoint::BreakPoint;
mod color;
pub use color::{Color, Opacity};
pub use color::{ColorBg, ColorText};
mod layout;
pub use layout::{ScaleSize, Side};
mod border;
pub use border::BorderColor;
mod rounded;
pub use rounded::RoundedRadius;
mod button;
pub use button::{ButtonColor, ButtonSize};

View file

@ -1,87 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::Color;
/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BorderColor {
/// No define ninguna clase.
#[default]
Default,
/// Genera internamente clases `border-{color}`.
Theme(Color),
/// Genera internamente clases `border-{color}-subtle` (un tono suavizado del color).
Subtle(Color),
/// Color negro.
Black,
/// Color blanco.
White,
}
impl BorderColor {
const BORDER: &str = "border";
const BORDER_PREFIX: &str = "border-";
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
// Añade la clase `border-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Subtle(c) => {
classes.push_str(Self::BORDER_PREFIX);
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::BORDER),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `border-*` correspondiente al color de borde.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
/// assert_eq!(BorderColor::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Subtle(c) => Self::BORDER_PREFIX.len() + c.as_str().len(),
_ => Self::BORDER.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Subtle(c) => {
class.push_str(Self::BORDER_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::BORDER),
}
class.push_str(suffix);
return class;
}
String::new()
}
}

View file

@ -1,114 +0,0 @@
use pagetop::prelude::*;
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum BreakPoint {
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
#[default]
None,
/// **576px o más** - Dispositivos pequeños: teléfonos en modo horizontal.
SM,
/// **768px o más** - Dispositivos medianos: tabletas.
MD,
/// **992px o más** - Dispositivos grandes: puestos de escritorio.
LG,
/// **1200px o más** - Dispositivos muy grandes: puestos de escritorio grandes.
XL,
/// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes.
XXL,
}
impl BreakPoint {
// Devuelve la identificación del punto de ruptura.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::None => "",
Self::SM => "sm",
Self::MD => "md",
Self::LG => "lg",
Self::XL => "xl",
Self::XXL => "xxl",
}
}
// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
// la cadena de clases.
//
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str, suffix: &str) {
if prefix.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::None => classes.push_str(prefix),
_ => {
classes.push_str(prefix);
classes.push('-');
classes.push_str(self.as_str());
}
}
if !suffix.is_empty() {
classes.push('-');
classes.push_str(suffix);
}
}
// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
// por un guion `-`.
//
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
// - Si `prefix` está vacío devuelve `""`.
//
// # Ejemplos
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// let bp = BreakPoint::MD;
// assert_eq!(bp.class_with("col", ""), "col-md");
// assert_eq!(bp.class_with("col", "6"), "col-md-6");
//
// let bp = BreakPoint::None;
// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
// assert_eq!(bp.class_with("col", "12"), "col-12");
//
// let bp = BreakPoint::LG;
// assert_eq!(bp.class_with("", "3"), "");
// ```
#[inline]
pub(crate) fn class_with(self, prefix: &str, suffix: &str) -> String {
if prefix.is_empty() {
return String::new();
}
let bp = self.as_str();
let has_bp = !bp.is_empty();
let has_suffix = !suffix.is_empty();
let mut len = prefix.len();
if has_bp {
len += 1 + bp.len();
}
if has_suffix {
len += 1 + suffix.len();
}
let mut class = String::with_capacity(len);
class.push_str(prefix);
if has_bp {
class.push('-');
class.push_str(bp);
}
if has_suffix {
class.push('-');
class.push_str(suffix);
}
class
}
}

View file

@ -1,143 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::Color;
// **< ButtonColor >********************************************************************************
/// Variantes de color `btn-*` para botones.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ButtonColor {
/// No define ninguna clase.
#[default]
Default,
/// Genera internamente clases `btn-{color}` (botón relleno).
Background(Color),
/// Genera `btn-outline-{color}` (fondo transparente y contorno con borde).
Outline(Color),
/// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto.
Link,
}
impl ButtonColor {
const BTN_PREFIX: &str = "btn-";
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
const BTN_LINK: &str = "btn-link";
// Añade la clase `btn-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Self::Default = self {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Default => unreachable!(),
Self::Background(c) => {
classes.push_str(Self::BTN_PREFIX);
classes.push_str(c.as_str());
}
Self::Outline(c) => {
classes.push_str(Self::BTN_OUTLINE_PREFIX);
classes.push_str(c.as_str());
}
Self::Link => {
classes.push_str(Self::BTN_LINK);
}
}
}
/// Devuelve la clase `btn-*` correspondiente al color del botón.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(
/// ButtonColor::Background(Color::Primary).to_class(),
/// "btn-primary"
/// );
/// assert_eq!(
/// ButtonColor::Outline(Color::Danger).to_class(),
/// "btn-outline-danger"
/// );
/// assert_eq!(ButtonColor::Link.to_class(), "btn-link");
/// assert_eq!(ButtonColor::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
match self {
Self::Default => String::new(),
Self::Background(c) => {
let color = c.as_str();
let mut class = String::with_capacity(Self::BTN_PREFIX.len() + color.len());
class.push_str(Self::BTN_PREFIX);
class.push_str(color);
class
}
Self::Outline(c) => {
let color = c.as_str();
let mut class = String::with_capacity(Self::BTN_OUTLINE_PREFIX.len() + color.len());
class.push_str(Self::BTN_OUTLINE_PREFIX);
class.push_str(color);
class
}
Self::Link => Self::BTN_LINK.to_string(),
}
}
}
// **< ButtonSize >*********************************************************************************
/// Tamaño visual de un botón.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ButtonSize {
/// Tamaño por defecto del tema (no añade clase).
#[default]
Default,
/// Botón compacto.
Small,
/// Botón destacado/grande.
Large,
}
impl ButtonSize {
const BTN_SM: &str = "btn-sm";
const BTN_LG: &str = "btn-lg";
// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Self::Default = self {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Default => unreachable!(),
Self::Small => classes.push_str(Self::BTN_SM),
Self::Large => classes.push_str(Self::BTN_LG),
}
}
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
/// assert_eq!(ButtonSize::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
match self {
Self::Default => String::new(),
Self::Small => Self::BTN_SM.to_string(),
Self::Large => Self::BTN_LG.to_string(),
}
}
}

View file

@ -1,375 +0,0 @@
use pagetop::prelude::*;
// **< Color >**************************************************************************************
/// Paleta de colores temáticos.
///
/// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`,
/// etc.). Este tipo enumerado sirve de base para componer las clases de color para fondo
/// ([`classes::Background`](crate::theme::classes::Background)), bordes
/// ([`classes::Border`](crate::theme::classes::Border)) y texto
/// ([`classes::Text`](crate::theme::classes::Text)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Color {
#[default]
Primary,
Secondary,
Success,
Info,
Warning,
Danger,
Light,
Dark,
}
impl Color {
// Devuelve el nombre del color.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Primary => "primary",
Self::Secondary => "secondary",
Self::Success => "success",
Self::Info => "info",
Self::Warning => "warning",
Self::Danger => "danger",
Self::Light => "light",
Self::Dark => "dark",
}
}
/* Añade el nombre del color a la cadena de clases (reservado).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(self.as_str());
} */
/// Devuelve la clase correspondiente al color.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(Color::Primary.to_class(), "primary");
/// assert_eq!(Color::Danger.to_class(), "danger");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.as_str().to_owned()
}
}
// **< Opacity >************************************************************************************
/// Niveles de opacidad (`opacity-*`).
///
/// Se usa normalmente para graduar la transparencia del color de fondo `bg-opacity-*`
/// ([`classes::Background`](crate::theme::classes::Background)), de los bordes `border-opacity-*`
/// ([`classes::Border`](crate::theme::classes::Border)) o del texto `text-opacity-*`
/// ([`classes::Text`](crate::theme::classes::Text)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Opacity {
/// No define ninguna clase.
#[default]
Default,
/// Permite generar clases `*-opacity-100` (100% de opacidad).
Opaque,
/// Permite generar clases `*-opacity-75` (75%).
SemiOpaque,
/// Permite generar clases `*-opacity-50` (50%).
Half,
/// Permite generar clases `*-opacity-25` (25%).
SemiTransparent,
/// Permite generar clases `*-opacity-10` (10%).
AlmostTransparent,
/// Permite generar clases `*-opacity-0` (0%, totalmente transparente).
Transparent,
}
impl Opacity {
const OPACITY: &str = "opacity";
const OPACITY_PREFIX: &str = "-opacity";
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Opaque => Some("-100"),
Self::SemiOpaque => Some("-75"),
Self::Half => Some("-50"),
Self::SemiTransparent => Some("-25"),
Self::AlmostTransparent => Some("-10"),
Self::Transparent => Some("-0"),
}
}
// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
// vacío para `opacity-*`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
if prefix.is_empty() {
classes.push_str(Self::OPACITY);
} else {
classes.push_str(prefix);
classes.push_str(Self::OPACITY_PREFIX);
}
classes.push_str(suffix);
}
}
// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
// `opacity-*`).
//
// # Ejemplos
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
// assert_eq!(Opacity::Default.class_with("bg"), "");
// ```
#[inline]
pub(crate) fn class_with(self, prefix: &str) -> String {
if let Some(suffix) = self.suffix() {
let base_len = if prefix.is_empty() {
Self::OPACITY.len()
} else {
prefix.len() + Self::OPACITY_PREFIX.len()
};
let mut class = String::with_capacity(base_len + suffix.len());
if prefix.is_empty() {
class.push_str(Self::OPACITY);
} else {
class.push_str(prefix);
class.push_str(Self::OPACITY_PREFIX);
}
class.push_str(suffix);
return class;
}
String::new()
}
/// Devuelve la clase de opacidad `opacity-*`.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
/// assert_eq!(Opacity::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.class_with("")
}
}
// **< ColorBg >************************************************************************************
/// Colores `bg-*` para el fondo.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorBg {
/// No define ninguna clase.
#[default]
Default,
/// Fondo predefinido del tema (`bg-body`).
Body,
/// Fondo predefinido del tema (`bg-body-secondary`).
BodySecondary,
/// Fondo predefinido del tema (`bg-body-tertiary`).
BodyTertiary,
/// Genera internamente clases `bg-{color}` (p. ej., `bg-primary`).
Theme(Color),
/// Genera internamente clases `bg-{color}-subtle` (un tono suavizado del color).
Subtle(Color),
/// Color negro.
Black,
/// Color blanco.
White,
/// No aplica ningún color de fondo (`bg-transparent`).
Transparent,
}
impl ColorBg {
const BG: &str = "bg";
const BG_PREFIX: &str = "bg-";
// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Body => Some("-body"),
Self::BodySecondary => Some("-body-secondary"),
Self::BodyTertiary => Some("-body-tertiary"),
Self::Theme(_) => Some(""),
Self::Subtle(_) => Some("-subtle"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
Self::Transparent => Some("-transparent"),
}
}
// Añade la clase de fondo `bg-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Subtle(c) => {
classes.push_str(Self::BG_PREFIX);
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::BG),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `bg-*` correspondiente al fondo.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
/// assert_eq!(ColorBg::Transparent.to_class(), "bg-transparent");
/// assert_eq!(ColorBg::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Subtle(c) => Self::BG_PREFIX.len() + c.as_str().len(),
_ => Self::BG.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Subtle(c) => {
class.push_str(Self::BG_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::BG),
}
class.push_str(suffix);
return class;
}
String::new()
}
}
// **< ColorText >**********************************************************************************
/// Colores `text-*` para el texto.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ColorText {
/// No define ninguna clase.
#[default]
Default,
/// Color predefinido del tema (`text-body`).
Body,
/// Color predefinido del tema (`text-body-emphasis`).
BodyEmphasis,
/// Color predefinido del tema (`text-body-secondary`).
BodySecondary,
/// Color predefinido del tema (`text-body-tertiary`).
BodyTertiary,
/// Genera internamente clases `text-{color}`.
Theme(Color),
/// Genera internamente clases `text-{color}-emphasis` (mayor contraste acorde al tema).
Emphasis(Color),
/// Color negro.
Black,
/// Color blanco.
White,
}
impl ColorText {
const TEXT: &str = "text";
const TEXT_PREFIX: &str = "text-";
// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Body => Some("-body"),
Self::BodyEmphasis => Some("-body-emphasis"),
Self::BodySecondary => Some("-body-secondary"),
Self::BodyTertiary => Some("-body-tertiary"),
Self::Theme(_) => Some(""),
Self::Emphasis(_) => Some("-emphasis"),
Self::Black => Some("-black"),
Self::White => Some("-white"),
}
}
// Añade la clase de texto `text-*` a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
match self {
Self::Theme(c) | Self::Emphasis(c) => {
classes.push_str(Self::TEXT_PREFIX);
classes.push_str(c.as_str());
}
_ => classes.push_str(Self::TEXT),
}
classes.push_str(suffix);
}
}
/// Devuelve la clase `text-*` correspondiente al color del texto.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(ColorText::Body.to_class(), "text-body");
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
/// assert_eq!(ColorText::Black.to_class(), "text-black");
/// assert_eq!(ColorText::Default.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
if let Some(suffix) = self.suffix() {
let base_len = match self {
Self::Theme(c) | Self::Emphasis(c) => Self::TEXT_PREFIX.len() + c.as_str().len(),
_ => Self::TEXT.len(),
};
let mut class = String::with_capacity(base_len + suffix.len());
match self {
Self::Theme(c) | Self::Emphasis(c) => {
class.push_str(Self::TEXT_PREFIX);
class.push_str(c.as_str());
}
_ => class.push_str(Self::TEXT),
}
class.push_str(suffix);
return class;
}
String::new()
}
}

View file

@ -1,104 +0,0 @@
use pagetop::prelude::*;
// **< ScaleSize >**********************************************************************************
/// Escala discreta de tamaños para definir clases utilitarias.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum ScaleSize {
/// Sin tamaño (no define ninguna clase).
#[default]
None,
/// Tamaño automático.
Auto,
/// Escala cero.
Zero,
/// Escala uno.
One,
/// Escala dos.
Two,
/// Escala tres.
Three,
/// Escala cuatro.
Four,
/// Escala cinco.
Five,
}
impl ScaleSize {
// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
// clase, o `""` para el tamaño automático.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Auto => Some(""),
Self::Zero => Some("-0"),
Self::One => Some("-1"),
Self::Two => Some("-2"),
Self::Three => Some("-3"),
Self::Four => Some("-4"),
Self::Five => Some("-5"),
}
}
// Añade el tamaño a la cadena de clases usando el prefijo dado.
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
if !prefix.is_empty() {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(prefix);
classes.push_str(suffix);
}
}
}
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
//
// # Ejemplo
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
// assert_eq!(ScaleSize::None.class_with("border"), "");
// ```
#[inline]
pub(crate) fn class_with(self, prefix: &str) -> String {
if !prefix.is_empty() {
if let Some(suffix) = self.suffix() {
let mut class = String::with_capacity(prefix.len() + suffix.len());
class.push_str(prefix);
class.push_str(suffix);
return class;
}
}
String::new()
} */
}
// **< Side >***************************************************************************************
/// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Side {
/// Todos los lados.
#[default]
All,
/// Lado superior.
Top,
/// Lado inferior.
Bottom,
/// Lado lógico de inicio (respetando RTL).
Start,
/// Lado lógico de fin (respetando RTL).
End,
/// Lados lógicos laterales (abreviatura *x*).
LeftAndRight,
/// Lados superior e inferior (abreviatura *y*).
TopAndBottom,
}

View file

@ -1,117 +0,0 @@
use pagetop::prelude::*;
/// Radio para el redondeo de esquinas ([`classes::Rounded`](crate::theme::classes::Rounded)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum RoundedRadius {
/// No define ninguna clase.
#[default]
None,
/// Genera `rounded` (radio por defecto del tema).
Default,
/// Genera `rounded-0` (sin redondeo).
Zero,
/// Genera `rounded-1`.
Scale1,
/// Genera `rounded-2`.
Scale2,
/// Genera `rounded-3`.
Scale3,
/// Genera `rounded-4`.
Scale4,
/// Genera `rounded-5`.
Scale5,
/// Genera `rounded-circle`.
Circle,
/// Genera `rounded-pill`.
Pill,
}
impl RoundedRadius {
const ROUNDED: &str = "rounded";
// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
// redondeo por defecto.
#[rustfmt::skip]
#[inline]
const fn suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Default => Some(""),
Self::Zero => Some("-0"),
Self::Scale1 => Some("-1"),
Self::Scale2 => Some("-2"),
Self::Scale3 => Some("-3"),
Self::Scale4 => Some("-4"),
Self::Scale5 => Some("-5"),
Self::Circle => Some("-circle"),
Self::Pill => Some("-pill"),
}
}
// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
// `rounded-bottom-start`, o vacío para `rounded-*`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
if let Some(suffix) = self.suffix() {
if !classes.is_empty() {
classes.push(' ');
}
if prefix.is_empty() {
classes.push_str(Self::ROUNDED);
} else {
classes.push_str(prefix);
}
classes.push_str(suffix);
}
}
// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
// `rounded-bottom-start`, o vacío para `rounded-*`).
//
// # Ejemplos
//
// ```rust
// # use pagetop_bootsier::prelude::*;
// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
// ```
#[inline]
pub(crate) fn class_with(self, prefix: &str) -> String {
if let Some(suffix) = self.suffix() {
let base_len = if prefix.is_empty() {
Self::ROUNDED.len()
} else {
prefix.len()
};
let mut class = String::with_capacity(base_len + suffix.len());
if prefix.is_empty() {
class.push_str(Self::ROUNDED);
} else {
class.push_str(prefix);
}
class.push_str(suffix);
return class;
}
String::new()
}
/// Devuelve la clase `rounded-*` para el redondeo de esquinas.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
/// assert_eq!(RoundedRadius::Circle.to_class(), "rounded-circle");
/// assert_eq!(RoundedRadius::None.to_class(), "");
/// ```
#[inline]
pub fn to_class(self) -> String {
self.class_with("")
}
}

View file

@ -1,13 +0,0 @@
//! Conjunto de clases para aplicar en componentes del tema.
mod color;
pub use color::{Background, Text};
mod border;
pub use border::Border;
mod rounded;
pub use rounded::Rounded;
mod layout;
pub use layout::{Margin, Padding};

View file

@ -1,175 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::{BorderColor, Opacity, ScaleSize, Side};
/// Clases para crear **bordes**.
///
/// Permite:
///
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
/// - Crear un borde con tamaño por defecto (`Border::new()`).
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
/// - Definir un tamaño **global** para todo el borde (`size`).
/// - Aplicar un **color** al borde (`BorderColor`).
/// - Aplicar un nivel de **opacidad** (`Opacity`).
///
/// # Comportamiento aditivo / sustractivo
///
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::default()` para ir
/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`.
///
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. usando
/// `classes::Border::new()` o `classes::Border::with(ScaleSize::Two)` y eliminar los lados
/// deseados con `ScaleSize::Zero`.
///
/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado.
///
/// # Ejemplos
///
/// **Borde global:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::with(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
/// ```
///
/// **Aditivo (solo borde superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
/// assert_eq!(b.to_class(), "border-top-1");
/// ```
///
/// **Sustractivo (borde global menos el superior):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
/// assert_eq!(b.to_class(), "border border-top-0");
/// ```
///
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::default()
/// .with_side(Side::Start, ScaleSize::Two)
/// .with_side(Side::End, ScaleSize::Four);
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
/// ```
///
/// **Combinado (ejemplo completo):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let b = classes::Border::new() // Borde por defecto.
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
/// .with_color(BorderColor::Theme(Color::Primary))
/// .with_opacity(Opacity::Half);
///
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Border {
all : ScaleSize,
top : ScaleSize,
end : ScaleSize,
bottom : ScaleSize,
start : ScaleSize,
color : BorderColor,
opacity: Opacity,
}
impl Border {
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
pub fn new() -> Self {
Self::with(ScaleSize::Auto)
}
/// Crea un borde **con un tamaño global** (`size`).
pub fn with(size: ScaleSize) -> Self {
Self::default().with_side(Side::All, size)
}
// **< Border BUILDER >*************************************************************************
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
match side {
Side::All => self.all = size,
Side::Top => self.top = size,
Side::Bottom => self.bottom = size,
Side::Start => self.start = size,
Side::End => self.end = size,
Side::LeftAndRight => {
self.start = size;
self.end = size;
}
Side::TopAndBottom => {
self.top = size;
self.bottom = size;
}
};
self
}
/// Establece el color del borde.
pub fn with_color(mut self, color: BorderColor) -> Self {
self.color = color;
self
}
/// Establece la opacidad del borde.
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
// **< Border HELPERS >*************************************************************************
/// Añade las clases de borde a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// *color* y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.all .push_class(classes, "border");
self.top .push_class(classes, "border-top");
self.end .push_class(classes, "border-end");
self.bottom .push_class(classes, "border-bottom");
self.start .push_class(classes, "border-start");
self.color .push_class(classes);
self.opacity.push_class(classes, "border");
}
/// Devuelve las clases de borde como cadena (`"border-2"`,
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
///
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
/// Atajo para crear un [`classes::Border`](crate::theme::classes::Border) a partir de un tamaño
/// [`ScaleSize`] aplicado a todo el borde.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// // Convertir explícitamente con `From::from`:
/// let b = classes::Border::from(ScaleSize::Two);
/// assert_eq!(b.to_class(), "border-2");
///
/// // Convertir implícitamente con `into()`:
/// let b: classes::Border = ScaleSize::Auto.into();
/// assert_eq!(b.to_class(), "border");
/// ```
impl From<ScaleSize> for Border {
fn from(size: ScaleSize) -> Self {
Self::with(size)
}
}

View file

@ -1,230 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::{ColorBg, ColorText, Opacity};
// **< Background >*********************************************************************************
/// Clases para establecer **color/opacidad del fondo**.
///
/// # Ejemplos
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Background::new();
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color de fondo.
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
/// assert_eq!(s.to_class(), "bg-primary");
///
/// // Color más opacidad.
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
/// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50");
///
/// // Usando `From<ColorBg>`.
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_class(), "bg-black");
///
/// // Usando `From<(ColorBg, Opacity)>`.
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Background {
color: ColorBg,
opacity: Opacity,
}
impl Background {
/// Prepara un nuevo estilo para aplicar al fondo.
pub fn new() -> Self {
Self::default()
}
/// Crea un estilo fijando el color de fondo (`bg-*`).
pub fn with(color: ColorBg) -> Self {
Self::default().with_color(color)
}
// **< Background BUILDER >*********************************************************************
/// Establece el color de fondo (`bg-*`).
pub fn with_color(mut self, color: ColorBg) -> Self {
self.color = color;
self
}
/// Establece la opacidad del fondo (`bg-opacity-*`).
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
// **< Background HELPERS >*********************************************************************
/// Añade las clases de fondo a la cadena de clases.
///
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`),
/// omitiendo los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "bg");
}
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
///
/// Si no se define ni color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
impl From<(ColorBg, Opacity)> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo y
/// la opacidad.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
/// ```
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
Background::with(color).with_opacity(opacity)
}
}
impl From<ColorBg> for Background {
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Background = ColorBg::Black.into();
/// assert_eq!(s.to_class(), "bg-black");
/// ```
fn from(color: ColorBg) -> Self {
Background::with(color)
}
}
// **< Text >***************************************************************************************
/// Clases para establecer **color/opacidad del texto**.
///
/// # Ejemplos
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// // Sin clases.
/// let s = classes::Text::new();
/// assert_eq!(s.to_class(), "");
///
/// // Sólo color del texto.
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
/// assert_eq!(s.to_class(), "text-primary");
///
/// // Color del texto y opacidad.
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
/// assert_eq!(s.to_class(), "text-white text-opacity-25");
///
/// // Usando `From<ColorText>`.
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_class(), "text-black");
///
/// // Usando `From<(ColorText, Opacity)>`.
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Text {
color: ColorText,
opacity: Opacity,
}
impl Text {
/// Prepara un nuevo estilo para aplicar al texto.
pub fn new() -> Self {
Self::default()
}
/// Crea un estilo fijando el color del texto (`text-*`).
pub fn with(color: ColorText) -> Self {
Self::default().with_color(color)
}
// **< Text BUILDER >***************************************************************************
/// Establece el color del texto (`text-*`).
pub fn with_color(mut self, color: ColorText) -> Self {
self.color = color;
self
}
/// Establece la opacidad del texto (`text-opacity-*`).
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
self.opacity = opacity;
self
}
// **< Text HELPERS >***************************************************************************
/// Añade las clases de texto a la cadena de clases.
///
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo los fragmentos vacíos.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.color.push_class(classes);
self.opacity.push_class(classes, "text");
}
/// Devuelve las clases de texto como cadena (`"text-primary"`, `"text-white text-opacity-25"`,
/// etc.).
///
/// Si no se define ni color ni opacidad, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}
impl From<(ColorText, Opacity)> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto y su opacidad.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
/// ```
fn from((color, opacity): (ColorText, Opacity)) -> Self {
Text::with(color).with_opacity(opacity)
}
}
impl From<ColorText> for Text {
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
/// texto.
///
/// # Ejemplo
///
/// ```
/// # use pagetop_bootsier::prelude::*;
/// let s: classes::Text = ColorText::Black.into();
/// assert_eq!(s.to_class(), "text-black");
/// ```
fn from(color: ColorText) -> Self {
Text::with(color)
}
}

View file

@ -1,205 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::{ScaleSize, Side};
use crate::theme::BreakPoint;
// **< Margin >*************************************************************************************
/// Clases para establecer **margin** por lado, tamaño y punto de ruptura.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
/// assert_eq!(m.to_class(), "mt-3");
///
/// let m = classes::Margin::with(Side::Start, ScaleSize::Auto).with_breakpoint(BreakPoint::LG);
/// assert_eq!(m.to_class(), "ms-lg-auto");
///
/// let m = classes::Margin::with(Side::All, ScaleSize::None);
/// assert_eq!(m.to_class(), "");
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Margin {
side: Side,
size: ScaleSize,
breakpoint: BreakPoint,
}
impl Margin {
/// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
/// ruptura.
pub fn with(side: Side, size: ScaleSize) -> Self {
Margin {
side,
size,
breakpoint: BreakPoint::None,
}
}
// **< Margin BUILDER >*************************************************************************
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **margin**.
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
self.breakpoint = breakpoint;
self
}
// **< Margin HELPERS >*************************************************************************
// Devuelve el prefijo `m*` según el lado.
#[rustfmt::skip]
#[inline]
const fn side_prefix(&self) -> &'static str {
match self.side {
Side::All => "m",
Side::Top => "mt",
Side::Bottom => "mb",
Side::Start => "ms",
Side::End => "me",
Side::LeftAndRight => "mx",
Side::TopAndBottom => "my",
}
}
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
#[rustfmt::skip]
#[inline]
const fn size_suffix(&self) -> Option<&'static str> {
match self.size {
ScaleSize::None => None,
ScaleSize::Auto => Some("auto"),
ScaleSize::Zero => Some("0"),
ScaleSize::One => Some("1"),
ScaleSize::Two => Some("2"),
ScaleSize::Three => Some("3"),
ScaleSize::Four => Some("4"),
ScaleSize::Five => Some("5"),
}
}
/* Añade la clase de **margin** a la cadena de clases (reservado).
//
// No añade nada si `size` es `ScaleSize::None`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let Some(size) = self.size_suffix() else {
return;
};
self.breakpoint
.push_class(classes, self.side_prefix(), size);
} */
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
///
/// Si `size` es `ScaleSize::None`, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let Some(size) = self.size_suffix() else {
return String::new();
};
self.breakpoint.class_with(self.side_prefix(), size)
}
}
// **< Padding >************************************************************************************
/// Clases para establecer **padding** por lado, tamaño y punto de ruptura.
///
/// # Ejemplos
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
/// assert_eq!(p.to_class(), "px-2");
///
/// let p = classes::Padding::with(Side::End, ScaleSize::Four).with_breakpoint(BreakPoint::SM);
/// assert_eq!(p.to_class(), "pe-sm-4");
///
/// let p = classes::Padding::with(Side::All, ScaleSize::Auto);
/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding.
/// ```
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Padding {
side: Side,
size: ScaleSize,
breakpoint: BreakPoint,
}
impl Padding {
/// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
/// ruptura.
pub fn with(side: Side, size: ScaleSize) -> Self {
Padding {
side,
size,
breakpoint: BreakPoint::None,
}
}
// **< Padding BUILDER >************************************************************************
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **padding**.
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
self.breakpoint = breakpoint;
self
}
// **< Padding HELPERS >************************************************************************
// Devuelve el prefijo `p*` según el lado.
#[rustfmt::skip]
#[inline]
const fn prefix(&self) -> &'static str {
match self.side {
Side::All => "p",
Side::Top => "pt",
Side::Bottom => "pb",
Side::Start => "ps",
Side::End => "pe",
Side::LeftAndRight => "px",
Side::TopAndBottom => "py",
}
}
// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
//
// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
#[rustfmt::skip]
#[inline]
const fn suffix(&self) -> Option<&'static str> {
match self.size {
ScaleSize::None => None,
ScaleSize::Auto => None,
ScaleSize::Zero => Some("0"),
ScaleSize::One => Some("1"),
ScaleSize::Two => Some("2"),
ScaleSize::Three => Some("3"),
ScaleSize::Four => Some("4"),
ScaleSize::Five => Some("5"),
}
}
/* Añade la clase de **padding** a la cadena de clases (reservado).
//
// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let Some(size) = self.suffix() else {
return;
};
self.breakpoint.push_class(classes, self.prefix(), size);
} */
// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
//
// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let Some(size) = self.suffix() else {
return String::new();
};
self.breakpoint.class_with(self.prefix(), size)
}
}

View file

@ -1,169 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::RoundedRadius;
/// Clases para definir **esquinas redondeadas**.
///
/// Permite:
///
/// - Definir un radio **global para todas las esquinas** (`radius`).
/// - Ajustar el radio asociado a las **esquinas de cada lado lógico** (`top`, `end`, `bottom`,
/// `start`, **en este orden**, respetando LTR/RTL).
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
///
/// # Ejemplos
///
/// **Radio global:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::with(RoundedRadius::Default);
/// assert_eq!(r.to_class(), "rounded");
/// ```
///
/// **Sin redondeo:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new();
/// assert_eq!(r.to_class(), "");
/// ```
///
/// **Radio en las esquinas de un lado lógico:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
/// assert_eq!(r.to_class(), "rounded-end-2");
/// ```
///
/// **Radio en una esquina concreta:**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
/// assert_eq!(r.to_class(), "rounded-top-start-3");
/// ```
///
/// **Combinado (ejemplo completo):**
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let r = classes::Rounded::new()
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
///
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
/// ```
#[rustfmt::skip]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub struct Rounded {
radius : RoundedRadius,
top : RoundedRadius,
end : RoundedRadius,
bottom : RoundedRadius,
start : RoundedRadius,
top_start : RoundedRadius,
top_end : RoundedRadius,
bottom_start: RoundedRadius,
bottom_end : RoundedRadius,
}
impl Rounded {
/// Prepara las esquinas **sin redondeo global** de partida.
pub fn new() -> Self {
Self::default()
}
/// Crea las esquinas **con redondeo global** (`radius`).
pub fn with(radius: RoundedRadius) -> Self {
Self::default().with_radius(radius)
}
// **< Rounded BUILDER >************************************************************************
/// Establece el radio global de las esquinas (`rounded*`).
pub fn with_radius(mut self, radius: RoundedRadius) -> Self {
self.radius = radius;
self
}
/// Establece el radio en las esquinas del lado superior (`rounded-top-*`).
pub fn with_top(mut self, radius: RoundedRadius) -> Self {
self.top = radius;
self
}
/// Establece el radio en las esquinas del lado lógico final (`rounded-end-*`). Respeta LTR/RTL.
pub fn with_end(mut self, radius: RoundedRadius) -> Self {
self.end = radius;
self
}
/// Establece el radio en las esquinas del lado inferior (`rounded-bottom-*`).
pub fn with_bottom(mut self, radius: RoundedRadius) -> Self {
self.bottom = radius;
self
}
/// Establece el radio en las esquinas del lado lógico inicial (`rounded-start-*`). Respeta
/// LTR/RTL.
pub fn with_start(mut self, radius: RoundedRadius) -> Self {
self.start = radius;
self
}
/// Establece el radio en la esquina superior-inicial (`rounded-top-start-*`). Respeta LTR/RTL.
pub fn with_top_start(mut self, radius: RoundedRadius) -> Self {
self.top_start = radius;
self
}
/// Establece el radio en la esquina superior-final (`rounded-top-end-*`). Respeta LTR/RTL.
pub fn with_top_end(mut self, radius: RoundedRadius) -> Self {
self.top_end = radius;
self
}
/// Establece el radio en la esquina inferior-inicial (`rounded-bottom-start-*`). Respeta
/// LTR/RTL.
pub fn with_bottom_start(mut self, radius: RoundedRadius) -> Self {
self.bottom_start = radius;
self
}
/// Establece el radio en la esquina inferior-final (`rounded-bottom-end-*`). Respeta LTR/RTL.
pub fn with_bottom_end(mut self, radius: RoundedRadius) -> Self {
self.bottom_end = radius;
self
}
// **< Rounded HELPERS >************************************************************************
/// Añade las clases de redondeo a la cadena de clases.
///
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
/// definiciones vacías.
#[rustfmt::skip]
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
self.radius .push_class(classes, "");
self.top .push_class(classes, "rounded-top");
self.end .push_class(classes, "rounded-end");
self.bottom .push_class(classes, "rounded-bottom");
self.start .push_class(classes, "rounded-start");
self.top_start .push_class(classes, "rounded-top-start");
self.top_end .push_class(classes, "rounded-top-end");
self.bottom_start.push_class(classes, "rounded-bottom-start");
self.bottom_end .push_class(classes, "rounded-bottom-end");
}
/// Devuelve las clases de redondeo como cadena (`"rounded"`,
/// `"rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"`, etc.).
///
/// Si no se define ningún radio, devuelve `""`.
#[inline]
pub fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
}
}

View file

@ -1,24 +0,0 @@
//! Definiciones para crear contenedores de componentes ([`Container`]).
//!
//! Cada contenedor envuelve contenido usando la etiqueta semántica indicada por
//! [`container::Kind`](crate::theme::container::Kind).
//!
//! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
//! fondo, texto, borde o esquinas redondeadas.
//!
//! # Ejemplo
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let main = Container::main()
//! .with_id("main-page")
//! .with_width(container::Width::From(BreakPoint::LG));
//! ```
mod props;
pub use props::{Kind, Width};
mod component;
pub use component::Container;

View file

@ -1,184 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Componente para crear un **contenedor de componentes**.
///
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
/// si existen componentes hijos (*children*).
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Container {
id : AttrId,
classes : AttrClasses,
container_kind : container::Kind,
container_width: container::Width,
children : Children,
}
impl Component for Container {
fn new() -> Self {
Container::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, self.width().to_class());
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let output = self.children().render(cx);
if output.is_empty() {
return PrepareMarkup::None;
}
let style = match self.width() {
container::Width::FluidMax(w) if w.is_measurable() => {
Some(join!("max-width: ", w.to_string(), ";"))
}
_ => None,
};
match self.container_kind() {
container::Kind::Default => PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
container::Kind::Main => PrepareMarkup::With(html! {
main id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
container::Kind::Header => PrepareMarkup::With(html! {
header id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
container::Kind::Footer => PrepareMarkup::With(html! {
footer id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
container::Kind::Section => PrepareMarkup::With(html! {
section id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
container::Kind::Article => PrepareMarkup::With(html! {
article id=[self.id()] class=[self.classes().get()] style=[style] {
(output)
}
}),
}
}
}
impl Container {
/// Crea un contenedor de tipo `Main` (`<main>`).
pub fn main() -> Self {
Container {
container_kind: container::Kind::Main,
..Default::default()
}
}
/// Crea un contenedor de tipo `Header` (`<header>`).
pub fn header() -> Self {
Container {
container_kind: container::Kind::Header,
..Default::default()
}
}
/// Crea un contenedor de tipo `Footer` (`<footer>`).
pub fn footer() -> Self {
Container {
container_kind: container::Kind::Footer,
..Default::default()
}
}
/// Crea un contenedor de tipo `Section` (`<section>`).
pub fn section() -> Self {
Container {
container_kind: container::Kind::Section,
..Default::default()
}
}
/// Crea un contenedor de tipo `Article` (`<article>`).
pub fn article() -> Self {
Container {
container_kind: container::Kind::Article,
..Default::default()
}
}
// **< Container BUILDER >**********************************************************************
/// Establece el identificador único (`id`) del contenedor.
#[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 contenedor.
///
/// También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`classes::Background`]).
/// - Definir la apariencia del texto ([`classes::Text`]).
/// - Establecer bordes ([`classes::Border`]).
/// - Redondear las esquinas ([`classes::Rounded`]).
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Establece el comportamiento del ancho para el contenedor.
#[builder_fn]
pub fn with_width(mut self, width: container::Width) -> Self {
self.container_width = width;
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 >**********************************************************************
/// Devuelve las clases CSS asociadas al contenedor.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el tipo semántico del contenedor.
pub fn container_kind(&self) -> &container::Kind {
&self.container_kind
}
/// Devuelve el comportamiento para el ancho del contenedor.
pub fn width(&self) -> &container::Width {
&self.container_width
}
/// Devuelve la lista de componentes (`children`) del contenedor.
pub fn children(&self) -> &Children {
&self.children
}
}

View file

@ -1,72 +0,0 @@
use pagetop::prelude::*;
use crate::theme::aux::BreakPoint;
// **< Kind >***************************************************************************************
/// Tipo de contenedor ([`Container`](crate::theme::Container)).
///
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
/// común a todos los contenedores.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// Contenedor genérico (`<div>`).
#[default]
Default,
/// Contenido principal de la página (`<main>`).
Main,
/// Encabezado de la página o de sección (`<header>`).
Header,
/// Pie de la página o de sección (`<footer>`).
Footer,
/// Sección de contenido (`<section>`).
Section,
/// Artículo de contenido (`<article>`).
Article,
}
// **< Width >**************************************************************************************
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::Container)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Width {
/// 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),
}
impl Width {
const CONTAINER: &str = "container";
/* Añade el comportamiento del contenedor a la cadena de clases según ancho (reservado).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
match self {
Self::Default => BreakPoint::None.push_class(classes, Self::CONTAINER, ""),
Self::From(bp) => bp.push_class(classes, Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
BreakPoint::None.push_class(classes, Self::CONTAINER, "fluid")
}
}
} */
/// Devuelve la clase asociada al comportamiento del contenedor según el ajuste de su ancho.
#[inline]
pub fn to_class(self) -> String {
match self {
Self::Default => BreakPoint::None.class_with(Self::CONTAINER, ""),
Self::From(bp) => bp.class_with(Self::CONTAINER, ""),
Self::Fluid | Self::FluidMax(_) => {
BreakPoint::None.class_with(Self::CONTAINER, "fluid")
}
}
}
}

View file

@ -1,34 +0,0 @@
//! 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, como 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).
//!
//! # Ejemplo
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let dd = Dropdown::new()
//! .with_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")));
//! ```
mod props;
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
mod component;
pub use component::Dropdown;
mod item;
pub use item::{Item, ItemKind};

View file

@ -1,302 +0,0 @@
use pagetop::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.
///
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
/// elementos sin ningún botón para interactuar.
///
/// Si este componente se usa en un menú [`Nav`] (ver [`nav::Item::dropdown()`]) sólo se tendrán en
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
/// resto de propiedades no afectarán a su representación en [`Nav`].
///
/// Ver ejemplo en el módulo [`dropdown`].
/// Si no contiene elementos, el componente **no se renderiza**.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Dropdown {
id : AttrId,
classes : AttrClasses,
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,
}
impl Component for Dropdown {
fn new() -> Self {
Dropdown::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(
ClassesOp::Prepend,
self.direction().class_with(self.button_grouped()),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
// Si no hay elementos en el menú, no se prepara.
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
// Título opcional para el menú desplegable.
let title = self.title().using(cx);
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
@if !title.is_empty() {
@let mut btn_classes = AttrClasses::new({
let mut classes = "btn".to_string();
self.button_size().push_class(&mut classes);
self.button_color().push_class(&mut classes);
classes
});
@let pos = self.menu_position();
@let offset = pos.data_offset();
@let reference = pos.data_reference();
@let auto_close = self.auto_close.as_str();
@let menu_classes = AttrClasses::new({
let mut classes = "dropdown-menu".to_string();
self.menu_align().push_class(&mut classes);
classes
});
// 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
type="button"
class=[btn_classes.get()]
{
(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-offset=[offset]
data-bs-reference=[reference]
data-bs-auto-close=[auto_close]
aria-expanded="false"
{
span class="visually-hidden" {
(L10n::t("dropdown_toggle", &LOCALES_BOOTSIER).using(cx))
}
}
};
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes).
@match self.direction() {
dropdown::Direction::Dropstart => {
(btn_toggle)
ul class=[menu_classes.get()] { (items) }
(btn)
}
_ => {
(btn)
(btn_toggle)
ul class=[menu_classes.get()] { (items) }
}
}
} @else {
// Botón único con funcionalidad de *toggle*.
button
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"
{
(title)
}
ul class=[menu_classes.get()] { (items) }
}
} @else {
// Sin botón: sólo el listado como menú contextual.
ul class="dropdown-menu" { (items) }
}
}
})
}
}
impl Dropdown {
// **< Dropdown BUILDER >***********************************************************************
/// Establece el identificador único (`id`) del menú desplegable.
#[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 menú desplegable.
#[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 menú desplegable.
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.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 {
self.items.add(Child::with(item));
self
}
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Dropdown GETTERS >***********************************************************************
/// Devuelve las clases CSS asociadas al menú desplegable.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el título del menú desplegable.
pub fn title(&self) -> &L10n {
&self.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 {
&self.items
}
}

View file

@ -1,281 +0,0 @@
use pagetop::prelude::*;
// **< ItemKind >***********************************************************************************
/// 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)]
pub enum ItemKind {
/// Elemento vacío, no produce salida.
#[default]
Void,
/// Etiqueta sin comportamiento interactivo.
Label(L10n),
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
/// inicialmente deshabilitado.
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 >***************************************************************************************
/// 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 elementos del menú.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
id : AttrId,
classes : AttrClasses,
item_kind: ItemKind,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.item_kind() {
ItemKind::Void => PrepareMarkup::None,
ItemKind::Label(label) => PrepareMarkup::With(html! {
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 is_current = !*disabled && (current_path == Some(path));
let mut classes = "dropdown-item".to_string();
if is_current {
classes.push_str(" active");
}
if *disabled {
classes.push_str(" disabled");
}
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))
}
}
})
}
ItemKind::Button { label, disabled } => {
let mut classes = "dropdown-item".to_string();
if *disabled {
classes.push_str(" disabled");
}
let aria_disabled = disabled.then_some("true");
let disabled_attr = disabled.then_some("disabled");
PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] {
button
class=(classes)
type="button"
aria-disabled=[aria_disabled]
disabled=[disabled_attr]
{
(label.using(cx))
}
}
})
}
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 {
/// Crea un elemento de tipo texto, mostrado sin interacción.
pub fn label(label: L10n) -> Self {
Item {
item_kind: ItemKind::Label(label),
..Default::default()
}
}
/// Crea un enlace para la navegación.
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: false,
disabled: false,
},
..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 {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: true,
disabled: false,
},
..Default::default()
}
}
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
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 button(label: L10n) -> Self {
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.
pub fn item_kind(&self) -> &ItemKind {
&self.item_kind
}
}

View file

@ -1,226 +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, Clone, Copy, Debug, PartialEq)]
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,
}
impl AutoClose {
// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::ClickableInside => Some("inside"),
Self::ClickableOutside => Some("outside"),
Self::ManualClose => Some("false"),
}
}
}
// **< Direction >**********************************************************************************
/// Dirección de despliegue de un menú [`Dropdown`].
///
/// Controla desde qué posición se muestra el menú respecto al botón.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
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,
}
impl Direction {
// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
#[rustfmt::skip ]
#[inline]
const fn as_str(self, grouped: bool) -> &'static str {
match self {
Self::Default if grouped => "",
Self::Default => "dropdown",
Self::Centered => "dropdown-center",
Self::Dropup => "dropup",
Self::DropupCentered => "dropup-center",
Self::Dropend => "dropend",
Self::Dropstart => "dropstart",
}
}
// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
// otros menús [`Dropdown`].
#[inline]
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
if grouped {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str("btn-group");
}
let class = self.as_str(grouped);
if !class.is_empty() {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
}
// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
// [`Dropdown`], o `""` si no corresponde ninguna.
#[inline]
pub(crate) fn class_with(self, grouped: bool) -> String {
let mut classes = String::new();
self.push_class(&mut classes, grouped);
classes
}
}
// **< 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, Clone, Copy, Debug, PartialEq)]
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),
}
impl MenuAlign {
#[inline]
fn push_one(classes: &mut String, class: &str) {
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
match self {
// Alineación por defecto (start), no añade clases extra.
Self::Start => {}
// `dropdown-menu-{bp}-start`
Self::StartAt(bp) => {
let class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &class);
}
// `dropdown-menu-start` + `dropdown-menu-{bp}-end`
Self::StartAndEnd(bp) => {
Self::push_one(classes, "dropdown-menu-start");
let bp_class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &bp_class);
}
// `dropdown-menu-end`
Self::End => {
Self::push_one(classes, "dropdown-menu-end");
}
// `dropdown-menu-{bp}-end`
Self::EndAt(bp) => {
let class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &class);
}
// `dropdown-menu-end` + `dropdown-menu-{bp}-start`
Self::EndAndStart(bp) => {
Self::push_one(classes, "dropdown-menu-end");
let bp_class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &bp_class);
}
}
}
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
} */
}
// **< 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, Clone, Copy, Debug, PartialEq)]
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,
}
impl MenuPosition {
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
#[inline]
pub(crate) fn data_offset(self) -> Option<String> {
match self {
Self::Offset(x, y) => Some(format!("{x},{y}")),
_ => None,
}
}
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
#[inline]
pub(crate) fn data_reference(self) -> Option<&'static str> {
match self {
Self::Parent => Some("parent"),
_ => None,
}
}
}

View file

@ -1,134 +0,0 @@
use crate::prelude::*;
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
#[derive(AutoDefault)]
pub enum IconKind {
#[default]
None,
Font(FontSize),
Svg {
shapes: Markup,
viewbox: AttrValue,
},
}
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Icon {
classes : AttrClasses,
icon_kind : IconKind,
aria_label: AttrL10n,
}
impl Component for Icon {
fn new() -> Self {
Icon::default()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
if !matches!(self.icon_kind(), IconKind::None) {
self.alter_classes(ClassesOp::Prepend, "icon");
}
if let IconKind::Font(font_size) = self.icon_kind() {
self.alter_classes(ClassesOp::Add, font_size.as_str());
}
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.icon_kind() {
IconKind::None => PrepareMarkup::None,
IconKind::Font(_) => {
let aria_label = self.aria_label().lookup(cx);
let has_label = aria_label.is_some();
PrepareMarkup::With(html! {
i
class=[self.classes().get()]
role=[has_label.then_some("img")]
aria-label=[aria_label]
aria-hidden=[(!has_label).then_some("true")]
{}
})
}
IconKind::Svg { shapes, viewbox } => {
let aria_label = self.aria_label().lookup(cx);
let has_label = aria_label.is_some();
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
PrepareMarkup::With(html! {
svg
xmlns="http://www.w3.org/2000/svg"
viewBox=(viewbox)
fill="currentColor"
focusable="false"
class=[self.classes().get()]
role=[has_label.then_some("img")]
aria-label=[aria_label]
aria-hidden=[(!has_label).then_some("true")]
{
(shapes)
}
})
}
}
}
}
impl Icon {
pub fn font() -> Self {
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
}
pub fn font_sized(font_size: FontSize) -> Self {
Icon::default().with_icon_kind(IconKind::Font(font_size))
}
pub fn svg(shapes: Markup) -> Self {
Icon::default().with_icon_kind(IconKind::Svg {
shapes,
viewbox: AttrValue::default(),
})
}
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
Icon::default().with_icon_kind(IconKind::Svg {
shapes,
viewbox: AttrValue::new(viewbox),
})
}
// **< Icon BUILDER >***************************************************************************
/// Modifica la lista de clases CSS aplicadas al icono.
#[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_icon_kind(mut self, icon_kind: IconKind) -> Self {
self.icon_kind = icon_kind;
self
}
#[builder_fn]
pub fn with_aria_label(mut self, label: L10n) -> Self {
self.aria_label.alter_value(label);
self
}
// **< Icon GETTERS >***************************************************************************
/// Devuelve las clases CSS asociadas al icono.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
pub fn icon_kind(&self) -> &IconKind {
&self.icon_kind
}
pub fn aria_label(&self) -> &AttrL10n {
&self.aria_label
}
}

View file

@ -1,7 +0,0 @@
//! Definiciones para renderizar imágenes ([`Image`]).
mod props;
pub use props::{Size, Source};
mod component;
pub use component::Image;

View file

@ -1,141 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Componente para renderizar una **imagen**.
///
/// - Ajusta su disposición según el origen definido en [`image::Source`].
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
/// ([`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`].
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Image {
id : AttrId,
classes: AttrClasses,
size : image::Size,
source : image::Source,
alt : AttrL10n,
}
impl Component for Image {
fn new() -> Self {
Image::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let dimensions = self.size().to_style();
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
let is_decorative = alt_text.is_empty();
let source = match self.source() {
image::Source::Logo(logo) => {
return PrepareMarkup::With(html! {
span
id=[self.id()]
class=[self.classes().get()]
style=[dimensions]
role=[(!is_decorative).then_some("img")]
aria-label=[(!is_decorative).then_some(alt_text)]
aria-hidden=[is_decorative.then_some("true")]
{
(logo.render(cx))
}
})
}
image::Source::Responsive(source) => Some(source),
image::Source::Thumbnail(source) => Some(source),
image::Source::Plain(source) => Some(source),
};
PrepareMarkup::With(html! {
img
src=[source]
alt=(alt_text)
id=[self.id()]
class=[self.classes().get()]
style=[dimensions] {}
})
}
}
impl Image {
/// Crea rápidamente una imagen especificando su origen.
pub fn with(source: image::Source) -> Self {
Image::default().with_source(source)
}
// **< Image BUILDER >**************************************************************************
/// Establece el identificador único (`id`) de la imagen.
#[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 a la imagen.
///
/// También acepta clases predefinidas para:
///
/// - Establecer bordes ([`classes::Border`]).
/// - Redondear las esquinas ([`classes::Rounded`]).
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
#[builder_fn]
pub fn with_size(mut self, size: image::Size) -> Self {
self.size = size;
self
}
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
#[builder_fn]
pub fn with_source(mut self, source: image::Source) -> Self {
self.source = source;
self
}
/// Define el texto alternativo localizado ([`L10n`]) para la imagen.
///
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
/// decorativa.
#[builder_fn]
pub fn with_alternative(mut self, alt: L10n) -> Self {
self.alt.alter_value(alt);
self
}
// **< Image GETTERS >**************************************************************************
/// Devuelve las clases CSS asociadas a la imagen.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve las dimensiones de la imagen.
pub fn size(&self) -> &image::Size {
&self.size
}
/// Devuelve el origen de la imagen.
pub fn source(&self) -> &image::Source {
&self.source
}
/// Devuelve el texto alternativo localizado.
pub fn alternative(&self) -> &AttrL10n {
&self.alt
}
}

View file

@ -1,108 +0,0 @@
use pagetop::prelude::*;
// **< Size >***************************************************************************************
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::Image)).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Size {
/// Ajuste automático por defecto.
///
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
#[default]
Auto,
/// Establece explícitamente el **ancho y alto** de la imagen.
///
/// Útil cuando se desea fijar ambas dimensiones de forma exacta. Ten en cuenta que la imagen
/// puede distorsionarse si no se mantiene la proporción original.
Dimensions(UnitValue, UnitValue),
/// Establece sólo el **ancho** de la imagen.
///
/// La altura se ajusta proporcionalmente de manera automática.
Width(UnitValue),
/// Establece sólo la **altura** de la imagen.
///
/// El ancho se ajusta proporcionalmente de manera automática.
Height(UnitValue),
/// Establece **el mismo valor** para el ancho y el alto de la imagen.
///
/// Práctico para forzar rápidamente un área cuadrada. Ten en cuenta que la imagen puede
/// distorsionarse si la original no es cuadrada.
Both(UnitValue),
}
impl Size {
// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
#[inline]
pub(crate) fn to_style(self) -> Option<String> {
match self {
Self::Auto => None,
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
Self::Width(w) => Some(format!("width: {w};")),
Self::Height(h) => Some(format!("height: {h};")),
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
}
}
}
// **< Source >*************************************************************************************
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::Image)).
#[derive(AutoDefault, Clone, Debug, PartialEq)]
pub enum Source {
/// Imagen con el logotipo de PageTop.
#[default]
Logo(PageTopSvg),
/// Imagen que se adapta automáticamente a su contenedor.
///
/// El `String` asociado es la URL (o ruta) de la imagen.
Responsive(String),
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
///
/// El `String` asociado es la URL (o ruta) de la imagen.
Thumbnail(String),
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
///
/// El `String` asociado es la URL (o ruta) de la imagen.
Plain(String),
}
impl Source {
const IMG_FLUID: &str = "img-fluid";
const IMG_THUMBNAIL: &str = "img-thumbnail";
// Devuelve la clase base asociada a la imagen según la fuente.
#[inline]
fn as_str(&self) -> &'static str {
match self {
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
Source::Plain(_) => "",
}
}
/* Añade la clase base asociada a la imagen según la fuente a la cadena de clases (reservado).
#[inline]
pub(crate) fn push_class(&self, classes: &mut String) {
let s = self.as_str();
if s.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(s);
} */
// Devuelve la clase asociada a la imagen según la fuente.
#[inline]
pub(crate) fn to_class(&self) -> String {
let s = self.as_str();
if s.is_empty() {
String::new()
} else {
let mut class = String::with_capacity(s.len());
class.push_str(s);
class
}
}
}

View file

@ -1,37 +0,0 @@
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
//!
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
//! desplegables [`Dropdown`](crate::theme::Dropdown).
//!
//! 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).
//!
//! # Ejemplo
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let nav = Nav::tabs()
//! .with_layout(nav::Layout::End)
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
//! .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")),
//! Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")),
//! ])),
//! ))
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
//! ```
mod props;
pub use props::{Kind, Layout};
mod component;
pub use component::Nav;
mod item;
pub use item::{Item, ItemKind};

View file

@ -1,135 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
///
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
///
/// Ver ejemplo en el módulo [`nav`].
/// Si no contiene elementos, el componente **no se renderiza**.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Nav {
id : AttrId,
classes : AttrClasses,
items : Children,
nav_kind : nav::Kind,
nav_layout: nav::Layout,
}
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, {
let mut classes = "nav".to_string();
self.nav_kind().push_class(&mut classes);
self.nav_layout().push_class(&mut classes);
classes
});
}
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 {
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
pub fn tabs() -> Self {
Nav::default().with_kind(nav::Kind::Tabs)
}
/// Crea un `Nav` usando botones para los elementos (*Pills*).
pub fn pills() -> Self {
Nav::default().with_kind(nav::Kind::Pills)
}
/// Crea un `Nav` usando elementos subrayados (*Underline*).
pub fn underline() -> Self {
Nav::default().with_kind(nav::Kind::Underline)
}
// **< Nav BUILDER >****************************************************************************
/// Establece el identificador único (`id`) del menú.
#[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 menú.
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
#[builder_fn]
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
self.nav_kind = kind;
self
}
/// Selecciona la distribución y orientación del menú.
#[builder_fn]
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
self.nav_layout = layout;
self
}
/// Añade un nuevo elemento hijo al menú.
pub fn add_item(mut self, item: nav::Item) -> Self {
self.items.add(Child::with(item));
self
}
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Nav GETTERS >****************************************************************************
/// Devuelve las clases CSS asociadas al menú.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el estilo visual seleccionado.
pub fn nav_kind(&self) -> &nav::Kind {
&self.nav_kind
}
/// Devuelve la distribución y orientación seleccionada.
pub fn nav_layout(&self) -> &nav::Layout {
&self.nav_layout
}
/// Devuelve la lista de elementos (`children`) del menú.
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -1,284 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
// **< ItemKind >***********************************************************************************
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
/// [`Nav`](crate::theme::Nav).
///
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
/// con él.
#[derive(AutoDefault)]
pub enum ItemKind {
/// Elemento vacío, no produce salida.
#[default]
Void,
/// Etiqueta sin comportamiento interactivo.
Label(L10n),
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
/// inicialmente deshabilitado.
Link {
label: L10n,
path: FnPathByContext,
blank: bool,
disabled: bool,
},
/// Elemento que despliega un menú [`Dropdown`].
Dropdown(Typed<Dropdown>),
}
impl ItemKind {
const ITEM: &str = "nav-item";
const DROPDOWN: &str = "nav-item dropdown";
// Devuelve las clases base asociadas al tipo de elemento.
#[inline]
const fn as_str(&self) -> &'static str {
match self {
Self::Void => "",
Self::Dropdown(_) => Self::DROPDOWN,
_ => Self::ITEM,
}
}
/* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado).
#[inline]
pub(crate) fn push_class(&self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
} */
// Devuelve las clases asociadas al tipo de elemento.
#[inline]
pub(crate) fn to_class(&self) -> String {
self.as_str().to_owned()
}
}
// **< Item >***************************************************************************************
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
///
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
/// puede comportarse como texto, enlace, botón o menú desplegable 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 elementos del menú.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Item {
id : AttrId,
classes : AttrClasses,
item_kind: ItemKind,
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.item_kind() {
ItemKind::Void => PrepareMarkup::None,
ItemKind::Label(label) => PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] {
span class="nav-link disabled" aria-disabled="true" {
(label.using(cx))
}
}
}),
ItemKind::Link {
label,
path,
blank,
disabled,
} => {
let path = path(cx);
let current_path = cx.request().map(|request| request.path());
let is_current = !*disabled && (current_path == Some(path));
let mut classes = "nav-link".to_string();
if is_current {
classes.push_str(" active");
}
if *disabled {
classes.push_str(" disabled");
}
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");
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]
{
(label.using(cx))
}
}
})
}
ItemKind::Dropdown(menu) => {
if let Some(dd) = menu.borrow() {
let items = dd.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
let title = dd.title().lookup(cx).unwrap_or_else(|| {
L10n::t("dropdown", &LOCALES_BOOTSIER)
.lookup(cx)
.unwrap_or_else(|| "Dropdown".to_string())
});
PrepareMarkup::With(html! {
li id=[self.id()] class=[self.classes().get()] {
a
class="nav-link dropdown-toggle"
data-bs-toggle="dropdown"
href="#"
role="button"
aria-expanded="false"
{
(title)
}
ul class="dropdown-menu" {
(items)
}
}
})
} else {
PrepareMarkup::None
}
}
}
}
}
impl Item {
/// Crea un elemento de tipo texto, mostrado sin interacción.
pub fn label(label: L10n) -> Self {
Item {
item_kind: ItemKind::Label(label),
..Default::default()
}
}
/// Crea un enlace para la navegación.
pub fn link(label: L10n, path: FnPathByContext) -> Self {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: false,
disabled: false,
},
..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 {
Item {
item_kind: ItemKind::Link {
label,
path,
blank: true,
disabled: false,
},
..Default::default()
}
}
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
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 elemento de navegación que contiene un menú desplegable [`Dropdown`].
///
/// Sólo se tienen en cuenta **el título** (si no existe le asigna uno por defecto) y **la lista
/// de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán a su
/// representación en [`Nav`].
pub fn dropdown(menu: Dropdown) -> Self {
Item {
item_kind: ItemKind::Dropdown(Typed::with(menu)),
..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.
pub fn item_kind(&self) -> &ItemKind {
&self.item_kind
}
}

View file

@ -1,120 +0,0 @@
use pagetop::prelude::*;
// **< Kind >***************************************************************************************
/// Define la variante de presentación de un menú [`Nav`](crate::theme::Nav).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Kind {
/// Estilo por defecto, lista de enlaces flexible y minimalista.
#[default]
Default,
/// Pestañas con borde para cambiar entre secciones.
Tabs,
/// Botones con fondo que resaltan el elemento activo.
Pills,
/// Variante con subrayado del elemento activo, estética ligera.
Underline,
}
impl Kind {
const TABS: &str = "nav-tabs";
const PILLS: &str = "nav-pills";
const UNDERLINE: &str = "nav-underline";
// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
match self {
Self::Default => "",
Self::Tabs => Self::TABS,
Self::Pills => Self::PILLS,
Self::Underline => Self::UNDERLINE,
}
}
// Añade la clase asociada al tipo de menú a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/* Devuelve la clase asociada al tipo de menú, o una cadena vacía si no aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
}
// **< Layout >*************************************************************************************
/// Distribución y orientación de un menú [`Nav`](crate::theme::Nav).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Layout {
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
#[default]
Default,
/// Alinea los elementos al inicio de la fila.
Start,
/// Centra horizontalmente los elementos.
Center,
/// Alinea los elementos al final de la fila.
End,
/// Apila los elementos en columna.
Vertical,
/// Los elementos se expanden para rellenar la fila.
Fill,
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
Justified,
}
impl Layout {
const START: &str = "justify-content-start";
const CENTER: &str = "justify-content-center";
const END: &str = "justify-content-end";
const VERTICAL: &str = "flex-column";
const FILL: &str = "nav-fill";
const JUSTIFIED: &str = "nav-justified";
// Devuelve la clase base asociada a la distribución y orientación del menú.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
match self {
Self::Default => "",
Self::Start => Self::START,
Self::Center => Self::CENTER,
Self::End => Self::END,
Self::Vertical => Self::VERTICAL,
Self::Fill => Self::FILL,
Self::Justified => Self::JUSTIFIED,
}
}
// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
// aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
}

View file

@ -1,136 +0,0 @@
//! Definiciones para crear barras de navegación [`Navbar`].
//!
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n).
//!
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
//!
//! # Ejemplos
//!
//! Barra **simple**, sólo con un menú horizontal:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let navbar = Navbar::simple()
//! .add_item(navbar::Item::nav(
//! Nav::new()
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about"))
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact"))
//! ));
//! ```
//!
//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let navbar = Navbar::simple_toggle()
//! .with_expand(BreakPoint::MD)
//! .add_item(navbar::Item::nav(
//! Nav::new()
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.example.com"))
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support"))
//! ));
//! ```
//!
//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let brand = navbar::Brand::new()
//! .with_title(L10n::n("PageTop"))
//! .with_path(Some(|_| "/"));
//!
//! let navbar = Navbar::brand_left(brand)
//! .add_item(navbar::Item::nav(
//! Nav::new()
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
//! .add_item(nav::Item::dropdown(
//! Dropdown::new()
//! .with_title(L10n::n("Tools"))
//! .add_item(dropdown::Item::link(L10n::n("Generator"), |_| "/tools/gen"))
//! .add_item(dropdown::Item::link(L10n::n("Reports"), |_| "/tools/reports"))
//! ))
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"))
//! ));
//! ```
//!
//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let brand = navbar::Brand::new()
//! .with_title(L10n::n("Intranet"))
//! .with_path(Some(|_| "/"));
//!
//! let navbar = Navbar::brand_right(brand)
//! .with_expand(BreakPoint::LG)
//! .add_item(navbar::Item::nav(
//! Nav::pills()
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard"))
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users"))
//! ));
//! ```
//!
//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let oc = Offcanvas::new()
//! .with_id("main_offcanvas")
//! .with_title(L10n::n("Main menu"))
//! .with_placement(offcanvas::Placement::Start)
//! .with_backdrop(offcanvas::Backdrop::Enabled);
//!
//! let navbar = Navbar::offcanvas(oc)
//! .add_item(navbar::Item::nav(
//! Nav::new()
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile"))
//! .add_item(nav::Item::dropdown(
//! Dropdown::new()
//! .with_title(L10n::n("More"))
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings"))
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help"))
//! ))
//! ));
//! ```
//!
//! Barra **fija arriba**:
//!
//! ```rust
//! # use pagetop::prelude::*;
//! # use pagetop_bootsier::prelude::*;
//! let brand = navbar::Brand::new()
//! .with_title(L10n::n("Main App"))
//! .with_path(Some(|_| "/"));
//!
//! let navbar = Navbar::brand_left(brand)
//! .with_position(navbar::Position::FixedTop)
//! .add_item(navbar::Item::nav(
//! Nav::new()
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/"))
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors"))
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock"))
//! ));
//! ```
mod props;
pub use props::{Layout, Position};
mod brand;
pub use brand::Brand;
mod component;
pub use component::Navbar;
mod item;
pub use item::Item;

View file

@ -1,111 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
///
/// Representa la identidad del sitio con una imagen, título y eslogan:
///
/// - Si hay URL ([`with_path()`](Self::with_path)), el bloque completo actúa como enlace. Por
/// defecto enlaza a la raíz del sitio (`/`).
/// - 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.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Brand {
id : AttrId,
image : Typed<Image>,
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
title : L10n,
slogan: L10n,
#[default(_code = "Some(|_| \"/\")")]
path : Option<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 image = self.image().render(cx);
let title = self.title().using(cx);
if title.is_empty() && image.is_empty() {
return PrepareMarkup::None;
}
let slogan = self.slogan().using(cx);
PrepareMarkup::With(html! {
@if let Some(path) = self.path() {
a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) }
} @else {
span class="navbar-brand" { (image) (title) (slogan) }
}
})
}
}
impl Brand {
// **< Brand BUILDER >**************************************************************************
/// Establece el identificador único (`id`) de la marca.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_value(id);
self
}
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
#[builder_fn]
pub fn with_image(mut self, image: Option<Image>) -> Self {
self.image.alter_component(image);
self
}
/// Establece el título de la identidad de marca.
#[builder_fn]
pub fn with_title(mut self, title: L10n) -> Self {
self.title = title;
self
}
/// Define el eslogan de la marca.
#[builder_fn]
pub fn with_slogan(mut self, slogan: L10n) -> Self {
self.slogan = slogan;
self
}
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
#[builder_fn]
pub fn with_path(mut self, path: Option<FnPathByContext>) -> Self {
self.path = path;
self
}
// **< Brand GETTERS >**************************************************************************
/// Devuelve la imagen de marca (si la hay).
pub fn image(&self) -> &Typed<Image> {
&self.image
}
/// Devuelve el título de la identidad de marca.
pub fn title(&self) -> &L10n {
&self.title
}
/// Devuelve el eslogan de la marca.
pub fn slogan(&self) -> &L10n {
&self.slogan
}
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
pub fn path(&self) -> &Option<FnPathByContext> {
&self.path
}
}

View file

@ -1,293 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
/// Componente para crear una **barra de navegación**.
///
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
///
/// Ver ejemplos en el módulo [`navbar`].
/// Si no contiene elementos, el componente **no se renderiza**.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Navbar {
id : AttrId,
classes : AttrClasses,
expand : BreakPoint,
layout : navbar::Layout,
position : navbar::Position,
items : Children,
}
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, {
let mut classes = "navbar".to_string();
self.expand().push_class(&mut classes, "navbar-expand", "");
self.position().push_class(&mut classes);
classes
});
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
// 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 = 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" {}
}
}
}
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
let items = self.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
// Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas.
let id = cx.required_id::<Self>(self.id());
PrepareMarkup::With(html! {
nav id=(id) class=[self.classes().get()] {
div class="container-fluid" {
@match self.layout() {
// Barra más sencilla: sólo contenido.
navbar::Layout::Simple => {
(items)
},
// Barra sencilla que se puede contraer/expandir.
navbar::Layout::SimpleToggle => {
@let id_content = join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con marca a la izquierda, siempre visible.
navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx))
(items)
},
// Barra con marca a la izquierda y botón a la derecha.
navbar::Layout::BrandLeft(brand) => {
@let id_content = join!(id, "-content");
(brand.render(cx))
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con botón a la izquierda y marca a la derecha.
navbar::Layout::BrandRight(brand) => {
@let id_content = join!(id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx))
div id=(id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
navbar::Layout::Offcanvas(offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.borrow() {
(oc.render_offcanvas(cx, Some(self.items())))
}
},
// Barra con marca a la izquierda y contenido en offcanvas.
navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx))
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.borrow() {
(oc.render_offcanvas(cx, Some(self.items())))
}
},
// Barra con contenido en offcanvas y marca a la derecha.
navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
(brand.render(cx))
@if let Some(oc) = offcanvas.borrow() {
(oc.render_offcanvas(cx, Some(self.items())))
}
},
}
}
}
})
}
}
impl Navbar {
/// Crea una barra de navegación **simple**, sin marca y sin botón.
pub fn simple() -> Self {
Navbar::default().with_layout(navbar::Layout::Simple)
}
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
pub fn simple_toggle() -> Self {
Navbar::default().with_layout(navbar::Layout::SimpleToggle)
}
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
Navbar::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::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 {
Navbar::default().with_layout(navbar::Layout::BrandLeft(Typed::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 {
Navbar::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
}
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
pub fn offcanvas(oc: Offcanvas) -> Self {
Navbar::default().with_layout(navbar::Layout::Offcanvas(Typed::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 {
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
Typed::with(brand),
Typed::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 {
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandRight(
Typed::with(brand),
Typed::with(oc),
))
}
// **< Navbar BUILDER >*************************************************************************
/// Establece el identificador único (`id`) de la barra de navegación.
#[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 a la barra de navegación.
///
/// También acepta clases predefinidas para:
///
/// - Modificar el color de fondo ([`classes::Background`]).
/// - Definir la apariencia del texto ([`classes::Text`]).
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_value(op, classes);
self
}
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
#[builder_fn]
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp;
self
}
/// Define el tipo de disposición que tendrá la barra de navegación.
#[builder_fn]
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
self.layout = layout;
self
}
/// Define dónde se mostrará la barra de navegación dentro del documento.
#[builder_fn]
pub fn with_position(mut self, position: navbar::Position) -> Self {
self.position = position;
self
}
/// Añade un nuevo contenido hijo.
#[inline]
pub fn add_item(mut self, item: navbar::Item) -> Self {
self.items.add(Child::with(item));
self
}
/// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`].
#[builder_fn]
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
self.items.alter_typed(op);
self
}
// **< Navbar GETTERS >*************************************************************************
/// Devuelve las clases CSS asociadas a la barra de navegación.
pub fn classes(&self) -> &AttrClasses {
&self.classes
}
/// Devuelve el punto de ruptura configurado.
pub fn expand(&self) -> &BreakPoint {
&self.expand
}
/// Devuelve la disposición configurada para la barra de navegación.
pub fn layout(&self) -> &navbar::Layout {
&self.layout
}
/// Devuelve la posición configurada para la barra de navegación.
pub fn position(&self) -> &navbar::Position {
&self.position
}
/// Devuelve la lista de contenidos (`children`).
pub fn items(&self) -> &Children {
&self.items
}
}

View file

@ -1,95 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
///
/// 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)]
pub enum Item {
/// Sin contenido, no produce salida.
#[default]
Void,
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
///
/// Ú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<navbar::Brand>),
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
Nav(Typed<Nav>),
/// Representa un texto libre localizado.
Text(L10n),
}
impl Component for Item {
fn new() -> Self {
Item::default()
}
fn id(&self) -> Option<String> {
match self {
Self::Void => None,
Self::Brand(brand) => brand.id(),
Self::Nav(nav) => nav.id(),
Self::Text(_) => None,
}
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
if let Self::Nav(nav) = self {
if let Some(mut nav) = nav.borrow_mut() {
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
}
}
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self {
Self::Void => PrepareMarkup::None,
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
Self::Nav(nav) => {
if let Some(nav) = nav.borrow() {
let items = nav.items().render(cx);
if items.is_empty() {
return PrepareMarkup::None;
}
PrepareMarkup::With(html! {
ul id=[nav.id()] class=[nav.classes().get()] {
(items)
}
})
} else {
PrepareMarkup::None
}
}
Self::Text(text) => PrepareMarkup::With(html! {
span class="navbar-text" {
(text.using(cx))
}
}),
}
}
}
impl Item {
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
///
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
/// desplegable.
pub fn brand(brand: navbar::Brand) -> Self {
Self::Brand(Typed::with(brand))
}
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
pub fn nav(item: Nav) -> Self {
Self::Nav(Typed::with(item))
}
/// Crea un elemento de texto localizado, mostrado sin interacción.
pub fn text(item: L10n) -> Self {
Self::Text(item)
}
}

View file

@ -1,98 +0,0 @@
use pagetop::prelude::*;
use crate::prelude::*;
// **< Layout >*************************************************************************************
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
#[derive(AutoDefault)]
pub enum Layout {
/// Barra simple, sin marca de identidad y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
#[default]
Simple,
/// Barra simple, con botón de despliegue a la izquierda y sin marca de identidad.
SimpleToggle,
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
///
/// La barra de navegación no se colapsa.
SimpleBrandLeft(Typed<navbar::Brand>),
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
BrandLeft(Typed<navbar::Brand>),
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
BrandRight(Typed<navbar::Brand>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
Offcanvas(Typed<Offcanvas>),
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
/// derecha.
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
/// derecha.
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
}
// **< Position >***********************************************************************************
/// Posición global de una barra de navegación [`Navbar`] en el documento.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Position {
/// Barra normal, fluye con el documento.
#[default]
Static,
/// Barra fijada en la parte superior, siempre visible.
///
/// Puede ser necesario reservar espacio en la parte superior del contenido que fluye debajo
/// para evitar que quede oculto por la barra.
FixedTop,
/// Barra fijada en la parte inferior, siempre visible.
///
/// Puede ser necesario reservar espacio en la parte inferior del contenido que fluye debajo
/// para evitar que quede oculto por la barra.
FixedBottom,
/// La barra de navegación se fija en la parte superior al hacer *scroll*.
StickyTop,
/// La barra de navegación se fija en la parte inferior al hacer *scroll*.
StickyBottom,
}
impl Position {
// Devuelve la clase base asociada a la posición de la barra de navegación.
#[inline]
const fn as_str(self) -> &'static str {
match self {
Self::Static => "",
Self::FixedTop => "fixed-top",
Self::FixedBottom => "fixed-bottom",
Self::StickyTop => "sticky-top",
Self::StickyBottom => "sticky-bottom",
}
}
// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
// aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_string()
} */
}

View file

@ -1,27 +0,0 @@
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
//!
//! # 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_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"))
//! );
//! ```
mod props;
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
mod component;
pub use component::Offcanvas;

View file

@ -1,240 +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.
///
/// Ver ejemplo en el módulo [`offcanvas`].
/// Si no contiene elementos, el componente **no se renderiza**.
#[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()
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.alter_classes(ClassesOp::Prepend, {
let mut classes = "offcanvas".to_string();
self.breakpoint().push_class(&mut classes, "offcanvas", "");
self.placement().push_class(&mut classes);
self.visibility().push_class(&mut classes);
classes
});
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(self.render_offcanvas(cx, None))
}
}
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
}
// **< Offcanvas HELPERS >**********************************************************************
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
let body = self.children().render(cx);
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {});
if body.is_empty() && body_extra.is_empty() {
return html! {};
}
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);
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)
(body_extra)
}
}
}
}
}

View file

@ -1,119 +0,0 @@
use pagetop::prelude::*;
// **< Backdrop >***********************************************************************************
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
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, Clone, Copy, Debug, PartialEq)]
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, Clone, Copy, Debug, PartialEq)]
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,
}
impl Placement {
// Devuelve la clase base asociada a la posición de aparición del panel.
#[rustfmt::skip]
#[inline]
const fn as_str(self) -> &'static str {
match self {
Placement::Start => "offcanvas-start",
Placement::End => "offcanvas-end",
Placement::Top => "offcanvas-top",
Placement::Bottom => "offcanvas-bottom",
}
}
// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(self.as_str());
}
/* Devuelve la clase asociada a la posición de aparición del panel (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
}
// **< Visibility >*********************************************************************************
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Visibility {
/// El panel permanece oculto desde el principio.
#[default]
Default,
/// El panel se muestra abierto al cargar.
Show,
}
impl Visibility {
// Devuelve la clase base asociada al estado inicial del panel.
#[inline]
const fn as_str(self) -> &'static str {
match self {
Visibility::Default => "",
Visibility::Show => "show",
}
}
// Añade la clase asociada al estado inicial del panel a la cadena de clases.
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
let class = self.as_str();
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
/* Devuelve la clase asociada al estado inicial, o una cadena vacía si no aplica (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
self.as_str().to_owned()
} */
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
/*!
* Bootstrap v5.3.8 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
(function (global, factory) {
@ -224,7 +224,7 @@
* @param {HTMLElement} element
* @return void
*
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = element => {
element.offsetHeight; // eslint-disable-line no-unused-expressions
@ -269,7 +269,7 @@
});
};
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
};
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
if (!waitForTransition) {
@ -591,7 +591,7 @@
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
for (const key of bsKeys) {
let pureKey = key.replace(/^bs/, '');
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
attributes[pureKey] = normalizeData(element.dataset[key]);
}
return attributes;
@ -666,7 +666,7 @@
* Constants
*/
const VERSION = '5.3.8';
const VERSION = '5.3.3';
/**
* Class definition
@ -692,8 +692,6 @@
this[propertyName] = null;
}
}
// Private
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated);
}
@ -1625,11 +1623,11 @@
this._element.style[dimension] = '';
this._queueCallback(complete, this._element, true);
}
// Private
_isShown(element = this._element) {
return element.classList.contains(CLASS_NAME_SHOW$7);
}
// Private
_configAfterMerge(config) {
config.toggle = Boolean(config.toggle); // Coerce string values
config.parent = getElement(config.parent);
@ -1883,7 +1881,7 @@
}
_createPopper() {
if (typeof Popper__namespace === 'undefined') {
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
}
let referenceElement = this._element;
if (this._config.reference === 'parent') {
@ -1962,7 +1960,7 @@
}
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
...execute(this._config.popperConfig, [defaultBsPopperConfig])
};
}
_selectMenuItem({
@ -2984,6 +2982,7 @@
*
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
*/
// eslint-disable-next-line unicorn/better-regex
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
const allowedAttribute = (attribute, allowedAttributeList) => {
const attributeName = attribute.nodeName.toLowerCase();
@ -3148,7 +3147,7 @@
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
}
_resolvePossibleFunction(arg) {
return execute(arg, [undefined, this]);
return execute(arg, [this]);
}
_putElementInTemplate(element, templateElement) {
if (this._config.html) {
@ -3247,7 +3246,7 @@
class Tooltip extends BaseComponent {
constructor(element, config) {
if (typeof Popper__namespace === 'undefined') {
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
}
super(element, config);
@ -3293,6 +3292,7 @@
if (!this._isEnabled) {
return;
}
this._activeTrigger.click = !this._activeTrigger.click;
if (this._isShown()) {
this._leave();
return;
@ -3480,7 +3480,7 @@
return offset;
}
_resolvePossibleFunction(arg) {
return execute(arg, [this._element, this._element]);
return execute(arg, [this._element]);
}
_getPopperConfig(attachment) {
const defaultBsPopperConfig = {
@ -3518,7 +3518,7 @@
};
return {
...defaultBsPopperConfig,
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
...execute(this._config.popperConfig, [defaultBsPopperConfig])
};
}
_setListeners() {
@ -3527,7 +3527,6 @@
if (trigger === 'click') {
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
const context = this._initializeOnDelegatedTarget(event);
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
context.toggle();
});
} else if (trigger !== TRIGGER_MANUAL) {
@ -4393,6 +4392,7 @@
}
// Private
_maybeScheduleHide() {
if (!this._config.autohide) {
return;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,22 +2,15 @@
$enable-grid-classes: false;
$enable-cssgrid: true;
// Opacity
.bg-opacity-0 {
--bs-bg-opacity: 0;
}
.border-opacity-0 {
--bs-border-opacity: 0;
}
.text-opacity-0 {
--bs-text-opacity: 0;
}
// Text opacity
.text-opacity-10 {
--bs-text-opacity: 0.1;
}
// Extending utilities
$utilities: map-merge(
$utilities,

View file

@ -1,55 +1,54 @@
@import "bootstrap-5.3.8/mixins/banner";
@import "bootstrap-5.3.3/mixins/banner";
@include bsBanner("");
// scss-docs-start import-stack
// Configuration
@import "bootstrap-5.3.8/functions";
@import "bootstrap-5.3.8/variables";
@import "bootstrap-5.3.8/variables-dark";
@import "bootstrap-5.3.8/maps";
@import "bootstrap-5.3.8/mixins";
@import "bootstrap-5.3.8/utilities";
@import "bootstrap-5.3.3/functions";
@import "bootstrap-5.3.3/variables";
@import "bootstrap-5.3.3/variables-dark";
@import "bootstrap-5.3.3/maps";
@import "bootstrap-5.3.3/mixins";
@import "bootstrap-5.3.3/utilities";
@import "custom";
// Layout & components
@import "bootstrap-5.3.8/root";
@import "bootstrap-5.3.8/reboot";
@import "bootstrap-5.3.8/type";
@import "bootstrap-5.3.8/images";
@import "bootstrap-5.3.8/containers";
@import "bootstrap-5.3.8/grid";
@import "bootstrap-5.3.8/tables";
@import "bootstrap-5.3.8/forms";
@import "bootstrap-5.3.8/buttons";
@import "bootstrap-5.3.8/transitions";
@import "bootstrap-5.3.8/dropdown";
@import "bootstrap-5.3.8/button-group";
@import "bootstrap-5.3.8/nav";
@import "bootstrap-5.3.8/navbar";
@import "bootstrap-5.3.8/card";
@import "bootstrap-5.3.8/accordion";
@import "bootstrap-5.3.8/breadcrumb";
@import "bootstrap-5.3.8/pagination";
@import "bootstrap-5.3.8/badge";
@import "bootstrap-5.3.8/alert";
@import "bootstrap-5.3.8/progress";
@import "bootstrap-5.3.8/list-group";
@import "bootstrap-5.3.8/close";
@import "bootstrap-5.3.8/toasts";
@import "bootstrap-5.3.8/modal";
@import "bootstrap-5.3.8/tooltip";
@import "bootstrap-5.3.8/popover";
@import "bootstrap-5.3.8/carousel";
@import "bootstrap-5.3.8/spinners";
@import "bootstrap-5.3.8/offcanvas";
@import "bootstrap-5.3.8/placeholders";
@import "bootstrap-5.3.3/root";
@import "bootstrap-5.3.3/reboot";
@import "bootstrap-5.3.3/type";
@import "bootstrap-5.3.3/images";
@import "bootstrap-5.3.3/containers";
@import "bootstrap-5.3.3/grid";
@import "bootstrap-5.3.3/tables";
@import "bootstrap-5.3.3/forms";
@import "bootstrap-5.3.3/buttons";
@import "bootstrap-5.3.3/transitions";
@import "bootstrap-5.3.3/dropdown";
@import "bootstrap-5.3.3/button-group";
@import "bootstrap-5.3.3/nav";
@import "bootstrap-5.3.3/navbar";
@import "bootstrap-5.3.3/card";
@import "bootstrap-5.3.3/accordion";
@import "bootstrap-5.3.3/breadcrumb";
@import "bootstrap-5.3.3/pagination";
@import "bootstrap-5.3.3/badge";
@import "bootstrap-5.3.3/alert";
@import "bootstrap-5.3.3/progress";
@import "bootstrap-5.3.3/list-group";
@import "bootstrap-5.3.3/close";
@import "bootstrap-5.3.3/toasts";
@import "bootstrap-5.3.3/modal";
@import "bootstrap-5.3.3/tooltip";
@import "bootstrap-5.3.3/popover";
@import "bootstrap-5.3.3/carousel";
@import "bootstrap-5.3.3/spinners";
@import "bootstrap-5.3.3/offcanvas";
@import "bootstrap-5.3.3/placeholders";
// Helpers
@import "bootstrap-5.3.8/helpers";
// Custom definitions
@import "customs";
@import "bootstrap-5.3.3/helpers";
// Utilities
@import "bootstrap-5.3.8/utilities/api";
@import "bootstrap-5.3.3/utilities/api";
// scss-docs-end import-stack

View file

@ -134,12 +134,17 @@
&:last-child { border-bottom: 0; }
// stylelint-disable selector-max-class
> .accordion-collapse,
> .accordion-header .accordion-button,
> .accordion-header .accordion-button.collapsed {
@include border-radius(0);
> .accordion-header .accordion-button {
&,
&.collapsed {
@include border-radius(0);
}
}
// stylelint-enable selector-max-class
> .accordion-collapse {
@include border-radius(0);
}
}
}

Some files were not shown because too many files have changed in this diff Show more