Compare commits
No commits in common. "main" and "legacy/2025-01-15" have entirely different histories.
main
...
legacy/202
754 changed files with 28422 additions and 28237 deletions
|
|
@ -1,79 +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="")
|
||||
| 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 = "^🔖", skip = true },
|
||||
{ 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" },
|
||||
]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[alias]
|
||||
ts = ["test", "--features", "testing"] # cargo ts
|
||||
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
||||
|
|
@ -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"
|
||||
]
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -8,4 +8,6 @@
|
|||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
.vscode
|
||||
|
||||
# Directorio de trabajo
|
||||
workdir
|
||||
|
|
|
|||
115
CHANGELOG.md
115
CHANGELOG.md
|
|
@ -1,115 +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.5.0 (2026-05-03)
|
||||
|
||||
PageTop 0.5.0 es la versión más ambiciosa hasta la fecha; concentra un largo periodo de trabajo en
|
||||
refactorizaciones, nuevas abstracciones y mejoras que sientan las bases para una API estable y
|
||||
robusta.
|
||||
|
||||
Algunos cambios pueden romper la compatibilidad con versiones anteriores. Se recomienda consultar la
|
||||
[documentación de PageTop](https://docs.rs/pagetop) para actualizar el código a un entorno más
|
||||
expresivo y mejor preparado para crecer hacia la versión 1.0. Entre estos cambios destacan:
|
||||
|
||||
- **Respuestas web completas**: soporte para páginas HTML, redirecciones HTTP, respuestas JSON,
|
||||
cookies, y página de bienvenida integrada.
|
||||
- **API de componentes consolidada**: ciclo de renderizado definitivo con `is_renderable`, manejo de
|
||||
errores con `ComponentError` o mensajes de estado con `StatusMessage`/`MessageLevel`.
|
||||
- **Temas hijo y macros de renderizado**: los temas pueden extenderse entre sí para sobrescribir el
|
||||
renderizado de cualquier componente con `render_component!` y `setup_component!`.
|
||||
- **Nueva acción `AlterMarkup`**: permite a extensiones y temas interceptar y transformar el HTML
|
||||
final de cualquier componente antes de entregarlo.
|
||||
- **Regiones y plantillas en temas**: los componentes `Region` y `Template` formalizan la gestión de
|
||||
regiones, respaldados por una API de `Children` e `InRegion` completamente revisada.
|
||||
- **Sistema de localización refactorizado**: nueva arquitectura interna con API más clara, mejor
|
||||
integración en el contexto y soporte robusto para múltiples idiomas.
|
||||
- **Tipos HTML consolidados**: unidades CSS, clase `Classes`, atributos HTML refactorizados y
|
||||
cadenas internas optimizadas con `CowStr`.
|
||||
- **Nuevas macros y utilidades de API pública**: macro `Getters` para exponer campos de componentes.
|
||||
- **Configuración tipada**: nuevas opciones de configuración enumeradas para el log y otros
|
||||
parámetros del sistema, con una gestión más expresiva y segura.
|
||||
- **Recursos estáticos y trazabilidad**: gestión de recursos estáticos integrada en el núcleo de
|
||||
PageTop y soporte para trazas y registro de eventos desde la propia librería.
|
||||
|
||||
## 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
|
||||
- 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
|
||||
130
CONTRIBUTING.md
130
CONTRIBUTING.md
|
|
@ -1,130 +0,0 @@
|
|||
# Guía de contribución a PageTop
|
||||
|
||||
Gracias por tu interés en contribuir a **PageTop** 🎉
|
||||
|
||||
Este documento describe **cómo participar en el desarrollo del proyecto**, el flujo de trabajo y las
|
||||
normas que permitan garantizar un historial limpio, trazable y sostenible a largo plazo.
|
||||
|
||||
Por favor, léelo completo antes de abrir una *issue* o una *pull request*.
|
||||
|
||||
|
||||
## 1. Repositorios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
> ⚠️ **Importante**
|
||||
> Aunque GitHub permite abrir *issues* y *pull requests*, **la integración del código se realiza
|
||||
> únicamente en el repositorio oficial**. GitHub actúa como repositorio espejo que se sincroniza
|
||||
> automáticamente para reflejar el mismo estado.
|
||||
|
||||
|
||||
## 2. Issues (incidencias, propuestas, preguntas)
|
||||
|
||||
Antes de abrir una *issue* **en GitHub**:
|
||||
|
||||
* comprueba que no exista ya una similar,
|
||||
* describe claramente el problema o propuesta,
|
||||
* incluye pasos de reproducción si se trata de un *bug*,
|
||||
* indica versión, entorno y contexto cuando sea relevante.
|
||||
|
||||
Las *issues* se usan para:
|
||||
|
||||
* informar de errores,
|
||||
* propuestas de mejora,
|
||||
* discusión técnica previa a cambios relevantes.
|
||||
|
||||
|
||||
## 3. Pull Requests (PRs)
|
||||
|
||||
### 3.1 Dónde abrirlas
|
||||
|
||||
Las *pull requests* se abren **en GitHub**, normalmente contra la rama `main`. GitHub es el punto de
|
||||
entrada recomendado para contribuciones externas.
|
||||
|
||||
### 3.2 Reglas generales para PRs
|
||||
|
||||
* Cada PR debe abordar **un único objetivo claro**.
|
||||
* Mantén el alcance lo más acotado posible.
|
||||
* Incluye descripción clara del cambio.
|
||||
* Si el PR corrige una *issue*, enlázala explícitamente.
|
||||
* Asegúrate de que el código compila y pasa las pruebas.
|
||||
|
||||
### 3.3 Revisión y aceptación
|
||||
|
||||
Todas las PRs son **revisadas manualmente** y pueden recibir comentarios o solicitudes de cambios.
|
||||
|
||||
Las PRs aceptadas se integran en el repositorio oficial, nunca directamente en GitHub, preservando
|
||||
siempre la **autoría original** del contribuidor.
|
||||
|
||||
|
||||
### 3.4. Cierre de Pull Requests y sincronización
|
||||
|
||||
Una vez que el cambio ha sido integrado en el repositorio oficial:
|
||||
|
||||
* La PR en GitHub se **cierra manualmente**.
|
||||
* Se añade un **mensaje estándar de cierre** indicando que el cambio ha sido integrado.
|
||||
* El repositorio de GitHub **se sincroniza automáticamente** como espejo.
|
||||
|
||||
|
||||
## 4. Estilo de código y calidad
|
||||
|
||||
* Sigue el estilo existente del proyecto.
|
||||
* Mantén los comentarios claros y precisos.
|
||||
* La documentación es parte del código: actualízala cuando sea necesario.
|
||||
* Cambios públicos o estructurales deben ir acompañados de documentación.
|
||||
|
||||
|
||||
## 5. Commits
|
||||
|
||||
PageTop usa la especificación **gitmoji** para los mensajes de *commit*. El formato recomendado es:
|
||||
|
||||
```text
|
||||
<propósito> [(ámbito opcional):] <mensaje>
|
||||
«LÍNEA EN BLANCO»
|
||||
Cuerpo opcional
|
||||
«LÍNEA EN BLANCO»
|
||||
Nota(s) al pie opcional(es) para referencias, incidencias o cambios incompatibles
|
||||
```
|
||||
|
||||
Ejemplos (no más de 50 caracteres en la primera línea, y no más de 80 en el resto):
|
||||
|
||||
* `📝 Actualiza la guía de contribución`
|
||||
* `♻️ (locale): Refactoriza sistema de localización`
|
||||
* Un mensaje completo:
|
||||
```
|
||||
🎨 (bootsier): Mejora la asignación de clases
|
||||
|
||||
- Simplifica la generación de clases CSS para componentes Bootstrap.
|
||||
- Elimina duplicidades en enums de estilos y centraliza la lógica de composición
|
||||
para reducir errores y facilitar mantenimiento.
|
||||
- Alinea los nombres de variantes con la documentación pública.
|
||||
|
||||
Refs: PR #123
|
||||
```
|
||||
|
||||
El emoji puede usarse en formato Unicode o como *shortcode*, por ejemplo `:sparkles:` en vez de ✨.
|
||||
|
||||
Consulta la especificación oficial en https://gitmoji.dev/specification
|
||||
|
||||
Durante la integración, los *commits* pueden ajustarse para adaptarse al historial del proyecto.
|
||||
|
||||
Un *commit* debe representar una unidad lógica de cambio. Usa mensajes claros y descriptivos.
|
||||
|
||||
|
||||
## 6. Comunicación y respeto
|
||||
|
||||
PageTop sigue un enfoque profesional y colaborativo:
|
||||
|
||||
* Sé respetuoso en revisiones y discusiones.
|
||||
* Acepta sugerencias técnicas como parte del proceso.
|
||||
* Recuerda que todas las contribuciones son revisadas con el objetivo de mejorar el proyecto.
|
||||
|
||||
Si tienes dudas sobre el proceso, abre una *issue* de tipo pregunta para tratar la cuestión en
|
||||
comunidad.
|
||||
|
||||
---
|
||||
|
||||
Gracias por contribuir a **PageTop** 🚀 Cada aportación contribuye a mejorar el proyecto.
|
||||
37
CREDITS.md
37
CREDITS.md
|
|
@ -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,15 +34,16 @@ 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*
|
||||
|
||||
|
||||
# 🎨 CSS
|
||||
# 📰 Plantillas
|
||||
|
||||
La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/)
|
||||
para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia
|
||||
[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
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
|
||||
# 🎨 Icono
|
||||
|
||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
|
|
|
|||
3265
Cargo.lock
generated
3265
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
104
Cargo.toml
104
Cargo.toml
|
|
@ -1,92 +1,50 @@
|
|||
[package]
|
||||
name = "pagetop"
|
||||
version = "0.5.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.1"
|
||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||
figlet-rs = "1.0"
|
||||
getter-methods = "2.0"
|
||||
itoa = "1.0"
|
||||
indexmap = "2.14"
|
||||
parking_lot = "0.12"
|
||||
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.14"
|
||||
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-minimal.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testing = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27"
|
||||
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-minimal",
|
||||
"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.13", 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.3", path = "helpers/pagetop-macros" }
|
||||
pagetop-minimal = { version = "0.1", path = "helpers/pagetop-minimal" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.1", 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.5", 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" }
|
||||
|
|
|
|||
156
MAINTAINERS.md
156
MAINTAINERS.md
|
|
@ -1,156 +0,0 @@
|
|||
# MAINTAINERS.md
|
||||
|
||||
## Guía para mantenedores de PageTop
|
||||
|
||||
Este documento describe **el flujo técnico interno de revisión e integración de contribuciones** en
|
||||
**PageTop**.
|
||||
|
||||
Está dirigido a **mantenedores del proyecto** y **no forma parte de la guía de contribución para
|
||||
usuarios externos**. Su objetivo es servir como **referencia operativa**, garantizando coherencia,
|
||||
trazabilidad y preservación de la autoría en un entorno con repositorios espejo.
|
||||
|
||||
|
||||
## 1. Repositorios y principios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
### Principios clave
|
||||
|
||||
* El repositorio oficial **es la única fuente de verdad** del historial.
|
||||
* **Nunca se realizan *merges* en GitHub**.
|
||||
* Toda integración definitiva se realiza en el repositorio oficial.
|
||||
* La autoría original debe preservarse siempre.
|
||||
|
||||
|
||||
## 2. Configuración local recomendada
|
||||
|
||||
El remoto `github` debe configurarse únicamente para operaciones de lectura (*fetch*), con la URL de
|
||||
*push* deshabilitada para evitar publicaciones accidentales en el repositorio espejo.
|
||||
|
||||
Estado esperado de `git remote -v`:
|
||||
|
||||
```text
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (fetch)
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (push)
|
||||
github git@github.com:manuelcillero/pagetop.git (fetch)
|
||||
github DISABLED (push)
|
||||
```
|
||||
|
||||
Convenciones usadas en este documento:
|
||||
|
||||
* `origin` -> Repositorio oficial
|
||||
* `github` -> Repositorio espejo
|
||||
|
||||
|
||||
## 3. Recepción y revisión de Pull Requests
|
||||
|
||||
Las PRs externas llegan por GitHub, normalmente contra la rama `main`.
|
||||
|
||||
Se asume que el repositorio local está configurado para recuperar PRs de GitHub como referencias
|
||||
remotas (`refs/pull/<N>/head`):
|
||||
|
||||
```bash
|
||||
git fetch github --prune
|
||||
git checkout -b pr-123 github/pr/123
|
||||
```
|
||||
|
||||
Antes de integrar:
|
||||
|
||||
* Revisar el código manualmente.
|
||||
* Verificar formato, análisis y pruebas:
|
||||
|
||||
```bash
|
||||
cargo fmt
|
||||
cargo clippy
|
||||
cargo test
|
||||
```
|
||||
|
||||
* Comprobar impacto en documentación.
|
||||
* Evaluar coherencia con la arquitectura y el estilo del proyecto.
|
||||
|
||||
Los cambios adicionales se solicitan o se aplican explicando claramente el motivo.
|
||||
|
||||
|
||||
## 4. Estrategia de integración
|
||||
|
||||
La integración **se realiza siempre en el repositorio oficial** (`origin`).
|
||||
|
||||
### 4.1 Estrategia por defecto: *rebase* + *fast-forward*
|
||||
|
||||
Esta es la **estrategia estándar y recomendada** en PageTop. Ventajas:
|
||||
|
||||
* conserva los commits originales,
|
||||
* preserva la autoría real de cada cambio,
|
||||
* mantiene un historial lineal y trazable,
|
||||
* facilita auditoría y depuración.
|
||||
|
||||
Procedimiento típico:
|
||||
|
||||
```bash
|
||||
git checkout pr-123
|
||||
git rebase main
|
||||
|
||||
# Resolver conflictos si los hay
|
||||
|
||||
git checkout main
|
||||
git merge --ff-only pr-123
|
||||
```
|
||||
|
||||
Si `merge --ff-only` falla, **no se debe continuar**, indica divergencias que deben resolverse antes
|
||||
de integrar la PR.
|
||||
|
||||
### 4.2 Estrategia excepcional: *Squash*
|
||||
|
||||
Sólo debe usarse cuando esté justificado:
|
||||
|
||||
* la PR contiene múltiples commits de prueba o ruido,
|
||||
* el historial aportado no es significativo,
|
||||
* el cambio es pequeño y autocontenido.
|
||||
|
||||
En este caso, se debe **preservar explícitamente la autoría**:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git merge --squash pr-123
|
||||
git commit --author="Nombre Apellido <email@ejemplo.com>"
|
||||
```
|
||||
|
||||
|
||||
### 4.3. Publicación en el repositorio oficial
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Este *push* representa la **integración definitiva** del cambio en la rama `main`.
|
||||
|
||||
|
||||
## 5. Cierre de la PR y sincronización
|
||||
|
||||
Tras integrar el cambio en el repositorio oficial, se cierra manualmente la PR en GitHub con un
|
||||
mensaje estándar:
|
||||
|
||||
```text
|
||||
Gracias por tu contribución.
|
||||
|
||||
Este cambio ha sido integrado en el repositorio oficial en `main` (`<hash>`).
|
||||
GitHub actúa como repositorio espejo sincronizado.
|
||||
```
|
||||
|
||||
|
||||
## 6. Principios de mantenimiento
|
||||
|
||||
* Priorizar **claridad y trazabilidad** frente a rapidez.
|
||||
* Mantener un historial legible y significativo.
|
||||
* Documentar cambios estructurales o públicos.
|
||||
* Tratar las contribuciones externas con respeto y transparencia.
|
||||
|
||||
---
|
||||
|
||||
Este documento puede evolucionar con el proyecto.
|
||||
|
||||
No se trata de imponer rigidez, sino de **capturar el conocimiento operativo real** de PageTop como
|
||||
guía práctica para el mantenimiento.
|
||||
143
README.md
143
README.md
|
|
@ -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>
|
||||
|
||||
[](#-license)
|
||||
[](https://docs.rs/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](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.
|
||||
componentes sin comprometer su funcionalidad.
|
||||
|
||||
|
||||
## ⚡️ Guía rápida
|
||||
# ⚡️ 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()
|
||||
}
|
||||
|
||||
|
|
@ -74,67 +73,62 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
|
|||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
||||
|
||||
|
||||
## 📂 Proyecto
|
||||
# 📂 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
|
||||
## Auxiliares
|
||||
|
||||
* **[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-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-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
||||
proporciona una colección de macros procedurales 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`.
|
||||
|
||||
* **[pagetop-minimal](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal)**,
|
||||
ofrece macros declarativas esenciales para optimizar tareas comunes como la composición de
|
||||
texto, la concatenación de cadenas y el manejo de colecciones clave-valor.
|
||||
## Extensiones
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
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-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`.
|
||||
|
||||
### Extensiones
|
||||
* **[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-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-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`.
|
||||
|
||||
* **[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.
|
||||
## Temas
|
||||
|
||||
* **[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.
|
||||
|
||||
## Aplicaciones
|
||||
|
||||
* **[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).
|
||||
|
||||
|
||||
## 🧪 Pruebas
|
||||
# 🚧 Advertencia
|
||||
|
||||
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:
|
||||
|
||||
| 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*. |
|
||||
|
||||
> **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`.
|
||||
`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**.
|
||||
|
||||
|
||||
## 🚧 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
|
||||
# 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
@ -148,28 +142,7 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
|
|||
el ecosistema Rust.
|
||||
|
||||
|
||||
## ✨ Contribuir
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
El repositorio de GitHub actúa como espejo y punto de entrada para:
|
||||
|
||||
* dar mayor visibilidad al proyecto,
|
||||
* facilitar la participación de la comunidad,
|
||||
* centralizar *issues* y *pull requests* externas.
|
||||
|
||||
Aunque GitHub permite abrir *pull requests*, **la integración del código se realiza únicamente en el
|
||||
repositorio oficial**. El repositorio de GitHub se sincroniza posteriormente para reflejar el mismo
|
||||
estado.
|
||||
|
||||
En todos los casos, se respeta la **autoría original** de las contribuciones integradas, tanto en el
|
||||
historial como en la documentación asociada al cambio.
|
||||
|
||||
Para conocer el proceso completo de participación, revisión e integración de cambios, consulta el
|
||||
archivo [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
# ✨ 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
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[log]
|
||||
tracing = "Info,pagetop=Debug"
|
||||
22
drust/Cargo.toml
Normal file
22
drust/Cargo.toml
Normal 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
112
drust/README.md
Normal 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>
|
||||
|
||||
[](#-license)
|
||||
[](https://crates.io/crates/drust)
|
||||
[](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
6
drust/config/common.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[app]
|
||||
name = "Drust"
|
||||
description = "A modern web Content Management System to share your world."
|
||||
|
||||
[database]
|
||||
db_type = "mysql"
|
||||
7
drust/config/default.toml
Normal file
7
drust/config/default.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[app]
|
||||
#theme = "Aliner"
|
||||
theme = "Bootsier"
|
||||
language = "es-ES"
|
||||
|
||||
[log]
|
||||
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||
40
drust/config/predefined.toml
Normal file
40
drust/config/predefined.toml
Normal 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
39
drust/src/main.rs
Normal 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>
|
||||
//!
|
||||
//! [](#-license)
|
||||
//! [](https://crates.io/crates/drust)
|
||||
//! [](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
|
||||
}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct FormControls;
|
||||
|
||||
impl Extension for FormControls {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(form_controls));
|
||||
}
|
||||
}
|
||||
|
||||
async fn form_controls(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
.with_title(L10n::t("title", &LOC))
|
||||
.with_slogan(L10n::t("slogan", &LOC))
|
||||
.with_button(None::<(L10n, FnPathByContext)>)
|
||||
// Bloque 1: casillas, interruptores y botones de opción.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_selections", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-selections")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Casillas e interruptores (form::Checkbox).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_checkbox", &LOC))
|
||||
.with_description(L10n::t("desc_checkbox", &LOC))
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("accept_terms")
|
||||
.with_label(L10n::t("label_terms", &LOC))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("accept_marketing")
|
||||
.with_label(L10n::t("label_marketing", &LOC))
|
||||
.with_checked(true)
|
||||
.with_inline(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("newsletter")
|
||||
.with_label(L10n::t("label_newsletter", &LOC))
|
||||
.with_inline(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::switch()
|
||||
.with_name("notifications")
|
||||
.with_label(L10n::t("label_notifications", &LOC))
|
||||
.with_checked(true)
|
||||
.with_reverse(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::switch()
|
||||
.with_name("dark_mode")
|
||||
.with_label(L10n::t("label_dark_mode", &LOC))
|
||||
.with_disabled(true),
|
||||
),
|
||||
)
|
||||
// Grupo de casillas de verificación (form::check::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_checkgroup", &LOC))
|
||||
.with_child(
|
||||
form::check::Field::new()
|
||||
.with_name("interests")
|
||||
.with_label(L10n::t("label_interests", &LOC))
|
||||
.with_help_text(L10n::t("help_interests", &LOC))
|
||||
.with_item(
|
||||
form::check::Item::new(
|
||||
"rust",
|
||||
L10n::t("check_rust", &LOC),
|
||||
)
|
||||
.with_checked(true),
|
||||
)
|
||||
.with_item(form::check::Item::new(
|
||||
"web",
|
||||
L10n::t("check_web", &LOC),
|
||||
))
|
||||
.with_item(form::check::Item::new(
|
||||
"ai",
|
||||
L10n::t("check_ai", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::check::Item::new(
|
||||
"games",
|
||||
L10n::t("check_games", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Botones de opción (form::radio::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_radio", &LOC))
|
||||
.with_child(
|
||||
form::radio::Field::new()
|
||||
.with_name("frequency")
|
||||
.with_label(L10n::t("label_frequency", &LOC))
|
||||
.with_item(form::radio::Item::new(
|
||||
"daily",
|
||||
L10n::t("radio_daily", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::radio::Item::new(
|
||||
"weekly",
|
||||
L10n::t("radio_weekly", &LOC),
|
||||
)
|
||||
.with_checked(true),
|
||||
)
|
||||
.with_item(form::radio::Item::new(
|
||||
"monthly",
|
||||
L10n::t("radio_monthly", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::radio::Item::new(
|
||||
"never",
|
||||
L10n::t("radio_never", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-selections"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Bloque 2: campos de texto, multilínea y rango.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_text", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-text")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Campos de texto (form::input::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_text", &LOC))
|
||||
.with_child(
|
||||
form::input::Field::text()
|
||||
.with_name("name")
|
||||
.with_label(L10n::t("label_name", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::email()
|
||||
.with_name("email")
|
||||
.with_label(L10n::t("label_email", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_email",
|
||||
&LOC,
|
||||
))
|
||||
.with_autocomplete(
|
||||
Some(form::Autocomplete::email()),
|
||||
)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::password()
|
||||
.with_name("password")
|
||||
.with_label(L10n::t("label_password", &LOC))
|
||||
.with_autocomplete(Some(
|
||||
form::Autocomplete::new_password(),
|
||||
))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::telephone()
|
||||
.with_name("phone")
|
||||
.with_label(L10n::t("label_phone", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_phone",
|
||||
&LOC,
|
||||
)),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::url()
|
||||
.with_name("website")
|
||||
.with_label(L10n::t("label_url", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_url", &LOC)),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::search()
|
||||
.with_name("search")
|
||||
.with_label(L10n::t("label_search", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_search",
|
||||
&LOC,
|
||||
)),
|
||||
),
|
||||
)
|
||||
// Área de texto (form::Textarea).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_textarea", &LOC))
|
||||
.with_child(
|
||||
form::Textarea::new()
|
||||
.with_name("comment")
|
||||
.with_label(L10n::t("label_comment", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_comment",
|
||||
&LOC,
|
||||
))
|
||||
.with_rows(Some(4))
|
||||
.with_help_text(L10n::t("help_comment", &LOC)),
|
||||
),
|
||||
)
|
||||
// Control deslizante (form::Range).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_range", &LOC))
|
||||
.with_child(
|
||||
form::Range::new()
|
||||
.with_name("rating")
|
||||
.with_label(L10n::t("label_rating", &LOC))
|
||||
.with_min(Some(1.0))
|
||||
.with_max(Some(10.0))
|
||||
.with_step(Some(1.0))
|
||||
.with_value(Some(5.0))
|
||||
.with_help_text(L10n::t("help_rating", &LOC)),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-text"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Bloque 3: listas de selección y etiquetas flotantes.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_lists", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-lists")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Listas de selección (form::select::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_select", &LOC))
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("language")
|
||||
.with_label(L10n::t("label_language", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"",
|
||||
L10n::t("select_choose", &LOC),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_group(
|
||||
form::select::Group::new(L10n::t(
|
||||
"select_group_europe",
|
||||
&LOC,
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"es",
|
||||
L10n::t("select_spanish", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"fr",
|
||||
L10n::t("select_french", &LOC),
|
||||
)),
|
||||
)
|
||||
.with_group(
|
||||
form::select::Group::new(L10n::t(
|
||||
"select_group_americas",
|
||||
&LOC,
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"en",
|
||||
L10n::t("select_english", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"pt",
|
||||
L10n::t("select_portuguese", &LOC),
|
||||
)),
|
||||
)
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"xx",
|
||||
L10n::t("select_disabled", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("technologies")
|
||||
.with_label(L10n::t("label_technologies", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"rust",
|
||||
L10n::n("Rust"),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"python",
|
||||
L10n::n("Python"),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(form::select::Item::new(
|
||||
"javascript",
|
||||
L10n::n("JavaScript"),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"go",
|
||||
L10n::n("Go"),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"typescript",
|
||||
L10n::n("TypeScript"),
|
||||
))
|
||||
.with_multiple(true)
|
||||
.with_rows(Some(4))
|
||||
.with_help_text(L10n::t("help_technologies", &LOC)),
|
||||
),
|
||||
)
|
||||
// Etiquetas flotantes.
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_floating", &LOC))
|
||||
.with_child(
|
||||
form::input::Field::text()
|
||||
.with_name("fl_name")
|
||||
.with_label(L10n::t("label_name", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
||||
.with_floating_label(true)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Textarea::new()
|
||||
.with_name("fl_comment")
|
||||
.with_label(L10n::t("label_comment", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_comment",
|
||||
&LOC,
|
||||
))
|
||||
.with_floating_label(true),
|
||||
)
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("fl_country")
|
||||
.with_label(L10n::t("label_country", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"",
|
||||
L10n::t("select_choose", &LOC),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(form::select::Item::new(
|
||||
"de",
|
||||
L10n::t("select_germany", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"es",
|
||||
L10n::t("select_spain", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"fr",
|
||||
L10n::t("select_france", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"pt",
|
||||
L10n::t("select_portugal", &LOC),
|
||||
))
|
||||
.with_floating_label(true)
|
||||
.with_required(true),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-lists"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&FormControls).run()?.await
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct IntroColors;
|
||||
|
||||
impl Extension for IntroColors {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(intro_colors));
|
||||
}
|
||||
}
|
||||
|
||||
async fn intro_colors(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
.with_title(L10n::n("PageTop"))
|
||||
.with_slogan(L10n::t("colors_slogan", &LOC))
|
||||
.with_button(None::<(L10n, FnPathByContext)>)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_1", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "2"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_2", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "3"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_3", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "4"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_4", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "5"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_5", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "6"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_6", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&IntroColors).run()?.await
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
title = Form controls
|
||||
slogan = Bootsier form components showcase
|
||||
block_selections = Checkboxes, switches and radio buttons
|
||||
block_text = Text fields, multiline and range
|
||||
block_lists = Select lists and floating labels
|
||||
|
||||
fieldset_text = Text fields
|
||||
label_name = Full name
|
||||
placeholder_name = e.g.: Jane Smith
|
||||
label_email = Email address
|
||||
placeholder_email = user@example.com
|
||||
label_password = Password
|
||||
label_phone = Phone number
|
||||
placeholder_phone = +1 555 000 0000
|
||||
label_url = Website
|
||||
placeholder_url = https://example.com
|
||||
label_search = Search
|
||||
placeholder_search = Search term...
|
||||
|
||||
fieldset_textarea = Multiline text
|
||||
label_comment = Comment
|
||||
placeholder_comment = Write your comment here...
|
||||
help_comment = Maximum 500 characters.
|
||||
|
||||
fieldset_select = Selection lists
|
||||
label_language = Language
|
||||
label_country = Country
|
||||
label_technologies = Preferred technologies
|
||||
help_technologies = Hold Ctrl (or Cmd on Mac) to select multiple options.
|
||||
select_choose = — Choose an option —
|
||||
select_group_europe = Europe
|
||||
select_spanish = Spanish
|
||||
select_french = French
|
||||
select_group_americas = Americas
|
||||
select_english = English
|
||||
select_portuguese = Portuguese
|
||||
select_disabled = Not available
|
||||
select_germany = Germany
|
||||
select_spain = Spain
|
||||
select_france = France
|
||||
select_portugal = Portugal
|
||||
|
||||
fieldset_checkbox = Checkboxes and switches
|
||||
desc_checkbox = This group shows standard checkboxes, inline checkboxes, reverse-aligned options, and toggle switches for binary choices.
|
||||
label_terms = I accept the terms and conditions
|
||||
label_marketing = Commercial emails (inline)
|
||||
label_newsletter = Newsletter (inline)
|
||||
label_notifications = Enable notifications (reverse)
|
||||
label_dark_mode = Dark mode (unavailable)
|
||||
|
||||
fieldset_radio = Radio buttons
|
||||
label_frequency = Newsletter frequency
|
||||
radio_daily = Daily
|
||||
radio_weekly = Weekly
|
||||
radio_monthly = Monthly
|
||||
radio_never = Never (disabled)
|
||||
|
||||
fieldset_checkgroup = Checkbox group
|
||||
label_interests = Areas of interest
|
||||
help_interests = Select all options that apply.
|
||||
check_rust = Rust programming
|
||||
check_web = Web development
|
||||
check_ai = Artificial intelligence
|
||||
check_games = Game development (disabled)
|
||||
|
||||
fieldset_floating = Floating labels
|
||||
|
||||
fieldset_range = Slider
|
||||
label_rating = Overall rating
|
||||
help_rating = From 1 (very poor) to 10 (excellent).
|
||||
|
||||
btn_submit = Submit
|
||||
btn_reset = Reset
|
||||
btn_cancel = Cancel
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
colors_slogan = Chromatic intro test
|
||||
colors_block = Block { $n } — intro-bg-block-{ $n }
|
||||
colors_val_1 = Background color: <code style="color: #8b2500">#FFB84B</code> — Amber gold.
|
||||
colors_val_2 = Background color: <code style="color: #8b2500">#FFC66F</code> — Light golden.
|
||||
colors_val_3 = Background color: <code style="color: #8b2500">#FFD493</code> — Pale golden.
|
||||
colors_val_4 = Background color: <code style="color: #8b2500">#FFE3B7</code> — Light peach.
|
||||
colors_val_5 = Background color: <code style="color: #8b2500">#FFF1DB</code> — Cream.
|
||||
colors_val_6 = Background color: <code style="color: #8b2500">#FFFFFF</code> — White.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
menus_item_label = Label
|
||||
menus_item_link = Link
|
||||
menus_item_blank = External link
|
||||
menus_item_disabled = Disabled link
|
||||
|
||||
menus_test_title = Dropdown
|
||||
|
||||
menus_dev_header = Intro
|
||||
menus_dev_getting_started = Getting started
|
||||
menus_dev_guides = Development guides
|
||||
menus_dev_forum = Developers forum
|
||||
|
||||
menus_sdk_header = Software Development Kits
|
||||
menus_sdk_rust = SDKs Rust
|
||||
menus_sdk_js = SDKs JavaScript
|
||||
menus_sdk_python = SDKs Python
|
||||
|
||||
menus_plugin_header = Plugins
|
||||
menus_plugin_auth = Rust Plugin Auth
|
||||
menus_plugin_cache = Rust Plugin Cache
|
||||
|
||||
menus_item_sign_up = Sign up
|
||||
menus_item_login = Login
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
title = Controles de formulario
|
||||
slogan = Componentes Bootsier para formularios
|
||||
block_selections = Casillas, interruptores y botones de opción
|
||||
block_text = Campos de texto, multilínea y rango
|
||||
block_lists = Listas de selección y etiquetas flotantes
|
||||
|
||||
fieldset_text = Campos de texto
|
||||
label_name = Nombre completo
|
||||
placeholder_name = Ej.: Ana García
|
||||
label_email = Correo electrónico
|
||||
placeholder_email = usuario@ejemplo.com
|
||||
label_password = Contraseña
|
||||
label_phone = Teléfono
|
||||
placeholder_phone = +34 600 000
|
||||
label_url = Sitio web
|
||||
placeholder_url = https://ejemplo.com
|
||||
label_search = Búsqueda
|
||||
placeholder_search = Término de búsqueda...
|
||||
|
||||
fieldset_textarea = Texto multilínea
|
||||
label_comment = Comentario
|
||||
placeholder_comment = Escribe tu comentario aquí...
|
||||
help_comment = Máximo 500 caracteres.
|
||||
|
||||
fieldset_select = Listas de selección
|
||||
label_language = Idioma
|
||||
label_country = País
|
||||
label_technologies = Tecnologías preferidas
|
||||
help_technologies = Mantén Ctrl (o Cmd en Mac) para seleccionar varias opciones.
|
||||
select_choose = — Elige una opción —
|
||||
select_group_europe = Europa
|
||||
select_spanish = Español
|
||||
select_french = Francés
|
||||
select_group_americas = América
|
||||
select_english = Inglés
|
||||
select_portuguese = Portugués
|
||||
select_disabled = No disponible
|
||||
select_germany = Alemania
|
||||
select_spain = España
|
||||
select_france = Francia
|
||||
select_portugal = Portugal
|
||||
|
||||
fieldset_checkbox = Casillas e interruptores
|
||||
desc_checkbox = Este grupo muestra casillas de verificación estándar, casillas en línea, opciones alineadas a la derecha e interruptores para elecciones binarias.
|
||||
label_terms = Acepto los términos y condiciones
|
||||
label_marketing = Emails comerciales (en línea)
|
||||
label_newsletter = Boletín (en línea)
|
||||
label_notifications = Activar notificaciones (invertida)
|
||||
label_dark_mode = Modo oscuro (no disponible)
|
||||
|
||||
fieldset_radio = Botones de opción
|
||||
label_frequency = Frecuencia del boletín
|
||||
radio_daily = Diario
|
||||
radio_weekly = Semanal
|
||||
radio_monthly = Mensual
|
||||
radio_never = Nunca (deshabilitado)
|
||||
|
||||
fieldset_checkgroup = Grupo de casillas
|
||||
label_interests = Áreas de interés
|
||||
help_interests = Selecciona todas las opciones que correspondan.
|
||||
check_rust = Programación en Rust
|
||||
check_web = Desarrollo web
|
||||
check_ai = Inteligencia artificial
|
||||
check_games = Desarrollo de videojuegos (deshabilitado)
|
||||
|
||||
fieldset_floating = Etiquetas flotantes
|
||||
|
||||
fieldset_range = Control deslizante
|
||||
label_rating = Valoración general
|
||||
help_rating = De 1 (muy malo) a 10 (excelente).
|
||||
|
||||
btn_submit = Enviar
|
||||
btn_reset = Restablecer
|
||||
btn_cancel = Cancelar
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
colors_slogan = Prueba de intro cromática
|
||||
colors_block = Bloque { $n } — intro-bg-block-{ $n }
|
||||
colors_val_1 = Color de fondo: <code style="color: #8b2500">#FFB84B</code> — Ámbar dorado.
|
||||
colors_val_2 = Color de fondo: <code style="color: #8b2500">#FFC66F</code> — Dorado claro.
|
||||
colors_val_3 = Color de fondo: <code style="color: #8b2500">#FFD493</code> — Dorado pálido.
|
||||
colors_val_4 = Color de fondo: <code style="color: #8b2500">#FFE3B7</code> — Melocotón claro.
|
||||
colors_val_5 = Color de fondo: <code style="color: #8b2500">#FFF1DB</code> — Crema.
|
||||
colors_val_6 = Color de fondo: <code style="color: #8b2500">#FFFFFF</code> — Blanco.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
menus_item_label = Etiqueta
|
||||
menus_item_link = Enlace
|
||||
menus_item_blank = Enlace externo
|
||||
menus_item_disabled = Enlace deshabilitado
|
||||
|
||||
menus_test_title = Desplegable
|
||||
|
||||
menus_dev_header = Introducción
|
||||
menus_dev_getting_started = Primeros pasos
|
||||
menus_dev_guides = Guías de desarrollo
|
||||
menus_dev_forum = Foro de desarrolladores
|
||||
|
||||
menus_sdk_header = Kits de Desarrollo Software
|
||||
menus_sdk_rust = SDKs de Rust
|
||||
menus_sdk_js = SDKs de JavaScript
|
||||
menus_sdk_python = SDKs de Python
|
||||
|
||||
menus_plugin_header = Plugins
|
||||
menus_plugin_auth = Plugin Rust de autenticación
|
||||
menus_plugin_cache = Plugin Rust de caché
|
||||
|
||||
menus_item_sign_up = Registrarse
|
||||
menus_item_login = Iniciar sesión
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct SuperMenu;
|
||||
|
||||
impl Extension for SuperMenu {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
||||
.with_expand(BreakPoint::LG)
|
||||
.with_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_link", &LOC), |cx| {
|
||||
cx.route("/")
|
||||
}))
|
||||
.with_item(nav::Item::link_blank(
|
||||
L10n::t("menus_item_blank", &LOC),
|
||||
|_| "https://docs.rs/pagetop".into(),
|
||||
))
|
||||
.with_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(L10n::t("menus_test_title", &LOC))
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_dev_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_dev_getting_started", &LOC),
|
||||
|cx| cx.route("/dev/getting-started"),
|
||||
))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_dev_guides", &LOC),
|
||||
|cx| cx.route("/dev/guides"),
|
||||
))
|
||||
.with_item(dropdown::Item::link_blank(
|
||||
L10n::t("menus_dev_forum", &LOC),
|
||||
|_| "https://forum.example.dev".into(),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_sdk_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_sdk_rust", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust"),
|
||||
))
|
||||
.with_item(dropdown::Item::link(L10n::t("menus_sdk_js", &LOC), |cx| {
|
||||
cx.route("/dev/sdks/js")
|
||||
}))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_sdk_python", &LOC),
|
||||
|cx| cx.route("/dev/sdks/python"),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_plugin_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_plugin_auth", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
||||
))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_plugin_cache", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
|
||||
.with_item(dropdown::Item::link_disabled(
|
||||
L10n::t("menus_item_disabled", &LOC),
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.with_item(nav::Item::link_disabled(
|
||||
L10n::t("menus_item_disabled", &LOC),
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.with_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.with_classes(
|
||||
ClassesOp::Add,
|
||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
||||
)
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_sign_up", &LOC), |cx| {
|
||||
cx.route("/auth/sign-up")
|
||||
}))
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_login", &LOC), |cx| {
|
||||
cx.route("/auth/login")
|
||||
})),
|
||||
));
|
||||
|
||||
InRegion::Global(&DefaultRegion::Header).add(
|
||||
Container::new()
|
||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||
.with_child(navbar_menu),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&SuperMenu).run()?.await
|
||||
}
|
||||
1
extensions/.gitignore
vendored
Normal file
1
extensions/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pagetop-aliner/**
|
||||
|
|
@ -1,15 +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.1.0 (2026-05-03)
|
||||
|
||||
### Añadido
|
||||
|
||||
- Versión inicial
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
||||
[](https://docs.rs/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](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.
|
||||
|
|
@ -1,128 +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>
|
||||
|
||||
[](https://docs.rs/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](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)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.with_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 before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,358 +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 {
|
||||
border-radius: 3px;
|
||||
font: normal normal 400 10px/1.2 monospace;
|
||||
vertical-align: middle;
|
||||
padding: 1px 3px;
|
||||
margin: 0 3px;
|
||||
background: #faa;
|
||||
color: #fff;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
*::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>"; }
|
||||
|
|
@ -1,21 +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.1.1 (2026-05-07)
|
||||
|
||||
### Cambiado
|
||||
|
||||
- Renombra módulo `aux` por `attrs` para evitar posibles conflictos en Windows (#11)
|
||||
|
||||
## 0.1.0 (2026-05-03)
|
||||
|
||||
### Añadido
|
||||
|
||||
- Versión inicial
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
[package]
|
||||
name = "pagetop-bootsier"
|
||||
version = "0.1.1"
|
||||
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"]
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -2,92 +2,30 @@
|
|||
|
||||
<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>
|
||||
|
||||
[](#-license)
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
## 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
|
||||
# 🚧 Advertencia
|
||||
|
||||
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()
|
||||
}
|
||||
```
|
||||
`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**.
|
||||
|
||||
|
||||
## 🚧 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
|
||||
# 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
143
extensions/pagetop-bootsier/src/bs.rs
Normal file
143
extensions/pagetop-bootsier/src/bs.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
172
extensions/pagetop-bootsier/src/bs/container.rs
Normal file
172
extensions/pagetop-bootsier/src/bs/container.rs
Normal 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
|
||||
}
|
||||
}
|
||||
5
extensions/pagetop-bootsier/src/bs/dropdown.rs
Normal file
5
extensions/pagetop-bootsier/src/bs/dropdown.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod component;
|
||||
pub use component::Dropdown;
|
||||
|
||||
mod item;
|
||||
pub use item::Item;
|
||||
99
extensions/pagetop-bootsier/src/bs/dropdown/component.rs
Normal file
99
extensions/pagetop-bootsier/src/bs/dropdown/component.rs
Normal 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
|
||||
}
|
||||
}
|
||||
109
extensions/pagetop-bootsier/src/bs/dropdown/item.rs
Normal file
109
extensions/pagetop-bootsier/src/bs/dropdown/item.rs
Normal 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
|
||||
}
|
||||
}
|
||||
110
extensions/pagetop-bootsier/src/bs/grid.rs
Normal file
110
extensions/pagetop-bootsier/src/bs/grid.rs
Normal 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, ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
103
extensions/pagetop-bootsier/src/bs/grid/component.rs
Normal file
103
extensions/pagetop-bootsier/src/bs/grid/component.rs
Normal 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
|
||||
}
|
||||
}
|
||||
119
extensions/pagetop-bootsier/src/bs/grid/item.rs
Normal file
119
extensions/pagetop-bootsier/src/bs/grid/item.rs
Normal 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
|
||||
}
|
||||
}
|
||||
157
extensions/pagetop-bootsier/src/bs/image.rs
Normal file
157
extensions/pagetop-bootsier/src/bs/image.rs
Normal 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
|
||||
}
|
||||
}
|
||||
11
extensions/pagetop-bootsier/src/bs/navbar.rs
Normal file
11
extensions/pagetop-bootsier/src/bs/navbar.rs
Normal 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};
|
||||
102
extensions/pagetop-bootsier/src/bs/navbar/brand.rs
Normal file
102
extensions/pagetop-bootsier/src/bs/navbar/brand.rs
Normal 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
|
||||
}
|
||||
}
|
||||
202
extensions/pagetop-bootsier/src/bs/navbar/component.rs
Normal file
202
extensions/pagetop-bootsier/src/bs/navbar/component.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
extensions/pagetop-bootsier/src/bs/navbar/item.rs
Normal file
113
extensions/pagetop-bootsier/src/bs/navbar/item.rs
Normal 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
|
||||
}
|
||||
}
|
||||
75
extensions/pagetop-bootsier/src/bs/navbar/nav.rs
Normal file
75
extensions/pagetop-bootsier/src/bs/navbar/nav.rs
Normal 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
|
||||
}
|
||||
}
|
||||
59
extensions/pagetop-bootsier/src/bs/offcanvas.rs
Normal file
59
extensions/pagetop-bootsier/src/bs/offcanvas.rs
Normal 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,
|
||||
}
|
||||
187
extensions/pagetop-bootsier/src/bs/offcanvas/component.rs
Normal file
187
extensions/pagetop-bootsier/src/bs/offcanvas/component.rs
Normal 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
|
||||
}
|
||||
}
|
||||
10
extensions/pagetop-bootsier/src/bs/utility.rs
Normal file
10
extensions/pagetop-bootsier/src/bs/utility.rs
Normal 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};
|
||||
102
extensions/pagetop-bootsier/src/bs/utility/border.rs
Normal file
102
extensions/pagetop-bootsier/src/bs/utility/border.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
119
extensions/pagetop-bootsier/src/bs/utility/color.rs
Normal file
119
extensions/pagetop-bootsier/src/bs/utility/color.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
78
extensions/pagetop-bootsier/src/bs/utility/opacity.rs
Normal file
78
extensions/pagetop-bootsier/src/bs/utility/opacity.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
116
extensions/pagetop-bootsier/src/bs/utility/rounded.rs
Normal file
116
extensions/pagetop-bootsier/src/bs/utility/rounded.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,169 +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>
|
||||
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](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)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.with_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::*;
|
||||
}
|
||||
|
||||
/// Plantillas que Bootsier añade.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum BootsierTemplate {
|
||||
/// Plantilla predeterminada de Bootsier.
|
||||
#[default]
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl Template for BootsierTemplate {
|
||||
fn render(&'static self, cx: &mut Context) -> Markup {
|
||||
match self {
|
||||
Self::Standard => theme::Container::new()
|
||||
.with_classes(ClassesOp::Add, "container-wrapper")
|
||||
.with_width(theme::container::Width::FluidMax(
|
||||
config::SETTINGS.bootsier.max_width,
|
||||
))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
(DefaultRegion::Header.render(cx))
|
||||
(DefaultRegion::Content.render(cx))
|
||||
(DefaultRegion::Footer.render(cx))
|
||||
}
|
||||
})),
|
||||
}
|
||||
.render(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&BootsierTemplate::Standard
|
||||
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 before_render_page_body(&self, page: &mut Page) {
|
||||
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(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
JavaScript::defer("/bootsier/js/bootstrap.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
.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",
|
||||
_ => "",
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404_description = Oops! Page Not Found
|
||||
e404_message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500_description = Oops! Unexpected Error
|
||||
e500_message = We're having an issue. Please report this error to an administrator.
|
||||
back_homepage = Back to homepage
|
||||
e404-description = Oops! Page Not Found
|
||||
e404-message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500-description = Oops! Unexpected Error
|
||||
e500-message = We're having an issue. Please report this error to an administrator.
|
||||
back-homepage = Back to homepage
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# form::Input
|
||||
input_required = This field is required
|
||||
# Offcanvas
|
||||
close = Close
|
||||
|
||||
# Navbar
|
||||
toggle = Toggle navigation
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
region_header = Header
|
||||
region_nav_branding = Navigation branding region
|
||||
region_nav_main = Main navigation region
|
||||
region_nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
region_breadcrumb = Breadcrumb
|
||||
region_content = Main content
|
||||
region_sidebar_first = Sidebar first
|
||||
region_sidebar_second = Sidebar second
|
||||
region_footer = Footer
|
||||
header = Header
|
||||
nav_branding = Navigation branding region
|
||||
nav_main = Main navigation region
|
||||
nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
breadcrumb = Breadcrumb
|
||||
content = Main content
|
||||
sidebar_first = Sidebar first
|
||||
sidebar_second = Sidebar second
|
||||
footer = Footer
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404_description = ¡Vaya! Página No Encontrada
|
||||
e404_message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500_description = ¡Vaya! Error Inesperado
|
||||
e500_message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back_homepage = Volver al inicio
|
||||
e404-description = ¡Vaya! Página No Encontrada
|
||||
e404-message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500-description = ¡Vaya! Error Inesperado
|
||||
e500-message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back-homepage = Volver al inicio
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# form::Input
|
||||
input_required = Este campo es obligatorio
|
||||
# Offcanvas
|
||||
close = Cerrar
|
||||
|
||||
# Navbar
|
||||
toggle = Mostrar/ocultar navegación
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
region_header = Cabecera
|
||||
region_nav_branding = Navegación y marca
|
||||
region_nav_main = Navegación principal
|
||||
region_nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
region_breadcrumb = Ruta de posicionamiento
|
||||
region_content = Contenido principal
|
||||
region_sidebar_first = Barra lateral primera
|
||||
region_sidebar_second = Barra lateral segunda
|
||||
region_footer = Pie
|
||||
header = Cabecera
|
||||
nav_branding = Navegación y marca
|
||||
nav_main = Navegación principal
|
||||
nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
breadcrumb = Ruta de posicionamiento
|
||||
content = Contenido principal
|
||||
sidebar_first = Barra lateral primera
|
||||
sidebar_second = Barra lateral segunda
|
||||
footer = Pie
|
||||
|
|
|
|||
|
|
@ -1,49 +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 attrs;
|
||||
pub use attrs::*;
|
||||
|
||||
pub mod classes;
|
||||
|
||||
// Button.
|
||||
mod button;
|
||||
pub use button::Button;
|
||||
|
||||
// Container.
|
||||
pub mod container;
|
||||
#[doc(inline)]
|
||||
pub use container::Container;
|
||||
|
||||
// Dropdown.
|
||||
pub mod dropdown;
|
||||
#[doc(inline)]
|
||||
pub use dropdown::Dropdown;
|
||||
|
||||
// Form.
|
||||
pub mod form;
|
||||
#[doc(inline)]
|
||||
pub use form::Form;
|
||||
|
||||
// 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;
|
||||
|
|
@ -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::{ButtonAction, ButtonColor, ButtonSize};
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::Color;
|
||||
|
||||
/// Esquema de color 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 la clase `border-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera la clase `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(), "");
|
||||
/// ```
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::Color;
|
||||
|
||||
// **< ButtonAction >*********************************************************************************
|
||||
|
||||
/// Comportamiento de un [`Button`](crate::theme::Button) al activarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonAction {
|
||||
/// Envía un formulario al servidor. Es el **tipo por defecto**.
|
||||
#[default]
|
||||
Submit,
|
||||
/// Restablece todos los campos de un formulario a sus valores iniciales.
|
||||
Reset,
|
||||
/// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse
|
||||
/// mediante JavaScript.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ButtonAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
ButtonAction::Submit => "submit",
|
||||
ButtonAction::Reset => "reset",
|
||||
ButtonAction::Plain => "button",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< ButtonColor >********************************************************************************
|
||||
|
||||
/// Esquema de color para [`Button`](crate::theme::Button).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonColor {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Genera la clase `btn-{color}` (botón sólido).
|
||||
Background(Color),
|
||||
/// Genera la clase `btn-outline-{color}` (fondo transparente con contorno coloreado).
|
||||
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::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),
|
||||
Self::Default => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(), "");
|
||||
/// ```
|
||||
pub fn to_class(self) -> String {
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
||||
// **< 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 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) {
|
||||
let class = match self {
|
||||
Self::Default => return,
|
||||
Self::Small => Self::BTN_SM,
|
||||
Self::Large => Self::BTN_LG,
|
||||
};
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/// 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(), "");
|
||||
/// ```
|
||||
pub fn to_class(self) -> String {
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
|
@ -1,336 +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 referencia para componer las clases de color para el fondo
|
||||
/// ([`classes::Background`](crate::theme::classes::Background)), los bordes
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) o para el 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",
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub 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 >************************************************************************************
|
||||
|
||||
/// Esquema de color 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 la clase `bg-{color}` (p. ej., `bg-primary`).
|
||||
Theme(Color),
|
||||
/// Genera la clase `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(), "");
|
||||
/// ```
|
||||
pub fn to_class(self) -> String {
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
||||
// **< ColorText >**********************************************************************************
|
||||
|
||||
/// Esquema de color 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 la clase `text-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera la clase `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(), "");
|
||||
/// ```
|
||||
pub fn to_class(self) -> String {
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
|
@ -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"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub 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,
|
||||
}
|
||||
|
|
@ -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"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub 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("")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::{ButtonAction, ButtonColor, ButtonSize};
|
||||
|
||||
/// Componente para crear un **botón**.
|
||||
///
|
||||
/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`,
|
||||
/// `reset` y botón genérico) y con la variedad de estilos del tema a través de [`ButtonColor`] y
|
||||
/// [`ButtonSize`].
|
||||
///
|
||||
/// El comportamiento del botón se establece al crearlo:
|
||||
///
|
||||
/// - [`Button::submit()`]: botón de envío (por defecto).
|
||||
/// - [`Button::reset()`]: botón de restablecimiento de valores.
|
||||
/// - [`Button::plain()`]: botón genérico sin comportamiento predeterminado.
|
||||
///
|
||||
/// El botón puede usarse dentro o fuera de un formulario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let save = Button::submit(L10n::n("Save"))
|
||||
/// .with_color(ButtonColor::Background(Color::Primary));
|
||||
///
|
||||
/// let cancel = Button::plain(L10n::n("Cancel"))
|
||||
/// .with_color(ButtonColor::Outline(Color::Secondary));
|
||||
///
|
||||
/// let clear = Button::reset(L10n::n("Clear"))
|
||||
/// .with_size(ButtonSize::Small);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del
|
||||
/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar
|
||||
/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// action: Option<String>, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Button {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del botón.
|
||||
classes: Classes,
|
||||
/// Devuelve el comportamiento del botón al activarse.
|
||||
kind: ButtonAction,
|
||||
/// Devuelve el esquema de color del botón.
|
||||
color: ButtonColor,
|
||||
/// Devuelve el tamaño visual del botón.
|
||||
size: ButtonSize,
|
||||
/// Devuelve el nombre del botón.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor del botón.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta del botón.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si el botón recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el botón está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Button {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
let mut classes = "btn".to_string();
|
||||
(*self.color()).push_class(&mut classes);
|
||||
(*self.size()).push_class(&mut classes);
|
||||
self.alter_classes(ClassesOp::Prepend, classes);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
button
|
||||
id=[self.id()]
|
||||
type=(self.kind())
|
||||
class=[self.classes().get()]
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
/// Crea un botón de **envío** (`type="submit"`).
|
||||
///
|
||||
/// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los
|
||||
/// datos al servidor.
|
||||
pub fn submit(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Submit,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón de **restablecimiento** (`type="reset"`).
|
||||
///
|
||||
/// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales.
|
||||
pub fn reset(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Reset,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un **botón genérico** (`type="button"`).
|
||||
///
|
||||
/// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede
|
||||
/// definirse mediante JavaScript.
|
||||
pub fn plain(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Plain,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Button BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al botón.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el esquema de color del botón.
|
||||
///
|
||||
/// Usa [`ButtonColor::Background`] para botones sólidos o [`ButtonColor::Outline`] para
|
||||
/// variantes con contorno.
|
||||
#[builder_fn]
|
||||
pub fn with_color(mut self, color: ButtonColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño visual del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_size(mut self, size: ButtonSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del botón (atributo `name`).
|
||||
///
|
||||
/// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par
|
||||
/// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor del botón (atributo `value`).
|
||||
///
|
||||
/// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón
|
||||
/// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::{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 `""`.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::{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 `""`.
|
||||
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 `""`.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::{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"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None`, devuelve `""`.
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.suffix() else {
|
||||
return String::new();
|
||||
};
|
||||
self.breakpoint.class_with(self.prefix(), size)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::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 `""`.
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,160 +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*).
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Container {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al contenedor.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
container_kind: container::Kind,
|
||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
||||
container_width: container::Width,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Container {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let output = self.children().render(cx);
|
||||
if output.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let style = match self.container_width() {
|
||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||
Some(util::join!("max-width: ", w.to_string(), ";"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(match self.container_kind() {
|
||||
container::Kind::Default => html! {
|
||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
},
|
||||
container::Kind::Main => html! {
|
||||
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
},
|
||||
container::Kind::Header => html! {
|
||||
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
},
|
||||
container::Kind::Footer => html! {
|
||||
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
},
|
||||
container::Kind::Section => html! {
|
||||
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
},
|
||||
container::Kind::Article => 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 {
|
||||
Self {
|
||||
container_kind: container::Kind::Main,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
||||
pub fn header() -> Self {
|
||||
Self {
|
||||
container_kind: container::Kind::Header,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
||||
pub fn footer() -> Self {
|
||||
Self {
|
||||
container_kind: container::Kind::Footer,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
||||
pub fn section() -> Self {
|
||||
Self {
|
||||
container_kind: container::Kind::Section,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
||||
pub fn article() -> Self {
|
||||
Self {
|
||||
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_id(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_classes(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 al contenedor o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::attrs::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.
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::divider())
|
||||
//! .with_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .with_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};
|
||||
|
|
@ -1,261 +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**.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Dropdown {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del menú desplegable.
|
||||
title: L10n,
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
button_size: ButtonSize,
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
button_color: ButtonColor,
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
button_split: bool,
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
button_grouped: bool,
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
auto_close: dropdown::AutoClose,
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
direction: dropdown::Direction,
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
menu_align: dropdown::MenuAlign,
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
menu_position: dropdown::MenuPosition,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Dropdown {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
self.direction().class_with(*self.button_grouped()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Si no hay elementos en el menú, no se prepara.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// Título opcional para el menú desplegable.
|
||||
let title = self.title().using(cx);
|
||||
|
||||
Ok(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
@if !title.is_empty() {
|
||||
@let mut btn_classes = Classes::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 = Classes::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_classes(
|
||||
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_classes(
|
||||
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_id(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_classes(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 al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`ChildOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// dropdown.with_item(dropdown::Item::link("Opción", "/ruta"));
|
||||
/// dropdown.with_item(ChildOp::AddMany(vec![
|
||||
/// dropdown::Item::link(...).into(),
|
||||
/// dropdown::Item::divider().into(),
|
||||
/// dropdown::Item::link(...).into(),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,276 +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, Clone, Debug)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por
|
||||
/// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente
|
||||
/// deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
route: 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 el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="dropdown-item-text" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let route_link = route(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(route_link.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(route_link);
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
h6 class="dropdown-header" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Divider => 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 {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
///
|
||||
/// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual
|
||||
/// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
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, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
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, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón de acción local, sin navegación asociada.
|
||||
pub fn button(label: L10n) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón deshabilitado.
|
||||
pub fn button_disabled(label: L10n) -> Self {
|
||||
Self {
|
||||
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 {
|
||||
Self {
|
||||
item_kind: ItemKind::Header(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un separador visual entre bloques de elementos.
|
||||
pub fn divider() -> Self {
|
||||
Self {
|
||||
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_id(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_classes(op, classes);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,225 +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.
|
||||
#[doc(hidden)]
|
||||
pub 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).
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
//! Definiciones para crear formularios ([`Form`]).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_bootsier::prelude::*;
|
||||
//!
|
||||
//! let form_login = Form::new()
|
||||
//! .with_id("login")
|
||||
//! .with_action("/login")
|
||||
//! .with_child(
|
||||
//! form::input::Field::email()
|
||||
//! .with_name("email")
|
||||
//! .with_label(L10n::n("Email"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::input::Field::password()
|
||||
//! .with_name("password")
|
||||
//! .with_label(L10n::n("Password"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::Checkbox::check()
|
||||
//! .with_name("remember")
|
||||
//! .with_label(L10n::n("Remember me")),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! Button::submit(L10n::n("Sign in"))
|
||||
//! .with_color(ButtonColor::Background(Color::Primary)),
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
||||
|
||||
mod component;
|
||||
pub use component::Form;
|
||||
|
||||
mod fieldset;
|
||||
pub use fieldset::Fieldset;
|
||||
|
||||
mod checkbox;
|
||||
pub use checkbox::Checkbox;
|
||||
|
||||
pub mod check;
|
||||
|
||||
pub mod radio;
|
||||
|
||||
pub mod select;
|
||||
|
||||
pub mod input;
|
||||
|
||||
mod textarea;
|
||||
pub use textarea::Textarea;
|
||||
|
||||
mod range;
|
||||
pub use range::Range;
|
||||
|
||||
mod hidden;
|
||||
pub use hidden::Hidden;
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
//! Definiciones para crear grupos de casillas de verificación (*check buttons*).
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Casilla de verificación individual de un [`form::check::Field`](Field).
|
||||
///
|
||||
/// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable
|
||||
/// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto.
|
||||
///
|
||||
/// El parámetro `name` de [`form::check::Item::new()`](Item::new) se combina con el `name` del
|
||||
/// grupo para componer el atributo `name` de la casilla. Por ejemplo, si el grupo tiene
|
||||
/// `name=interests` y el ítem se crea con `name=tech`, la casilla tendrá `name=interests_tech`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el nombre que se combina con el del grupo para componer el atributo `name`.
|
||||
name: AttrValue,
|
||||
/// Devuelve la etiqueta de la casilla.
|
||||
label: L10n,
|
||||
/// Devuelve si la casilla debe aparecer marcada por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si la casilla está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea una nueva casilla con el nombre y la etiqueta indicados.
|
||||
///
|
||||
/// El parámetro `name` se combina con el del grupo para componer el atributo `name` de la
|
||||
/// casilla.
|
||||
pub fn new(name: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
name: AttrValue::new(name),
|
||||
label,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si la casilla debe aparecer marcada por defecto.
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la casilla está deshabilitada.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **grupo de casillas de verificación**.
|
||||
///
|
||||
/// Renderiza un conjunto de casillas de verificación donde, a diferencia de un grupo de botones
|
||||
/// [`form::radio::Field`](crate::theme::form::radio::Field), cada casilla puede marcarse de forma
|
||||
/// independiente.
|
||||
///
|
||||
/// Las casillas se añaden mediante [`with_item()`](Field::with_item) usando instancias de
|
||||
/// [`form::check::Item`](Item). Si se activa el modo en línea con
|
||||
/// [`with_inline()`](Field::with_inline), las casillas se disponen horizontalmente.
|
||||
///
|
||||
/// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo
|
||||
/// y el `name` del [`form::check::Item`](Item) con un guion bajo. Por ejemplo, para el grupo con
|
||||
/// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y
|
||||
/// `name=interests_tech`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let interests = form::check::Field::new()
|
||||
/// .with_name("interests")
|
||||
/// .with_label(L10n::n("Areas of interest"))
|
||||
/// .with_item(form::check::Item::new("art", L10n::n("Art")))
|
||||
/// .with_item(form::check::Item::new("tech", L10n::n("Technology")))
|
||||
/// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true));
|
||||
/// ```
|
||||
///
|
||||
/// Cada `name` debe ser único y válido como identificador de campo. Cuando el usuario marca una
|
||||
/// casilla, el navegador envía algo como `interests_tech=true`; mientras que si no la marca, no
|
||||
/// envía nada. En el servidor cada campo se deserializa como `bool` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// interests_art: bool,
|
||||
/// #[serde(default)]
|
||||
/// interests_tech: bool,
|
||||
/// #[serde(default)]
|
||||
/// interests_science: bool,
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del grupo.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre base compartido por todas las casillas del grupo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del grupo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del grupo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las casillas del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si todo el grupo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si las casillas se muestran en línea horizontalmente.
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-checkboxes");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-label" { (label) }
|
||||
}
|
||||
@let item_classes = if *self.inline() {
|
||||
"form-check form-check-inline"
|
||||
} else {
|
||||
"form-check"
|
||||
};
|
||||
@for (item, i) in self.items().iter().zip(1..) {
|
||||
@let i = i.to_string();
|
||||
@let item_id = util::join!(&container_id, "-check-", &i);
|
||||
@let item_name = if let Some(item_name) = item.name().get() {
|
||||
util::join!(&name, "_", &item_name)
|
||||
} else {
|
||||
util::join!(&name, "_", &i)
|
||||
};
|
||||
div class=(item_classes) {
|
||||
input
|
||||
type="checkbox"
|
||||
id=(&item_id)
|
||||
class="form-check-input"
|
||||
name=(&item_name)
|
||||
value="true"
|
||||
checked[*item.checked()]
|
||||
disabled[*item.disabled() || *self.disabled()];
|
||||
label class="form-check-label" for=(&item_id) {
|
||||
(item.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del grupo de casillas.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de casillas.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre base para el grupo de casillas.
|
||||
///
|
||||
/// Se combina con el `name` de cada [`form::check::Item`](Item) para generar el atributo `name`
|
||||
/// de cada casilla de verificación. Por ejemplo, con `name=interests` en el grupo y `name=tech`
|
||||
/// en el ítem, se genera `name=interests_tech`.
|
||||
///
|
||||
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
|
||||
/// el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade una casilla al grupo. Las casillas se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si todo el grupo está deshabilitado.
|
||||
///
|
||||
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si las casillas se muestran en línea horizontalmente.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada casilla.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
|
||||
///
|
||||
/// Renderiza un control binario (marcado/no marcado) en dos variantes visuales, por defecto se
|
||||
/// muestra como una casilla de verificación estándar, pero también puede renderizarse como un
|
||||
/// interruptor de encendido/apagado ([`Checkbox::switch()`]).
|
||||
///
|
||||
/// Se puede mostrar en línea con otros controles usando [`with_inline()`](Checkbox::with_inline), o
|
||||
/// justificar a la derecha del contenedor invirtiendo el orden de la etiqueta y el control usando
|
||||
/// [`with_reverse()`](Checkbox::with_reverse).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
|
||||
/// .with_name("terms_accepted")
|
||||
/// .with_label(L10n::n("I accept the terms and conditions"))
|
||||
/// .with_required(true);
|
||||
///
|
||||
/// let notifications = form::Checkbox::switch()
|
||||
/// .with_name("notifications_enabled")
|
||||
/// .with_label(L10n::n("Receive email notifications"))
|
||||
/// .with_checked(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el control está activo, el navegador envía `name=true`; si no lo está, no envía nada.
|
||||
/// En el servidor el campo se deserializa como `bool` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// terms_accepted: bool, // true = marcada, false = no marcada.
|
||||
/// #[serde(default)]
|
||||
/// notifications_enabled: bool, // true = activo, false = inactivo.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Checkbox {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del control.
|
||||
classes: Classes,
|
||||
/// Devuelve la variante visual del control.
|
||||
checkbox_kind: form::CheckboxKind,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del control.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si el control debe estar marcado/activo por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si el control recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el campo es obligatorio.
|
||||
required: bool,
|
||||
/// Devuelve si el control está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si el control se muestra en línea con otros controles.
|
||||
inline: bool,
|
||||
/// Devuelve si el control y su etiqueta se justifican a la derecha del contenedor.
|
||||
reverse: bool,
|
||||
}
|
||||
|
||||
impl Component for Checkbox {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
let mut classes = "form-field form-check".to_string();
|
||||
if *self.checkbox_kind() == form::CheckboxKind::Switch {
|
||||
classes.push_str(" form-switch");
|
||||
}
|
||||
if *self.inline() {
|
||||
classes.push_str(" form-check-inline");
|
||||
}
|
||||
if *self.reverse() {
|
||||
classes.push_str(" form-check-reverse");
|
||||
}
|
||||
self.alter_classes(ClassesOp::Prepend, classes);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 1));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
let checkbox_id = util::join!(&container_id, "-checkbox");
|
||||
let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch;
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
input
|
||||
type="checkbox"
|
||||
role=[is_switch.then_some("switch")]
|
||||
id=(&checkbox_id)
|
||||
class="form-check-input"
|
||||
name=(&name)
|
||||
value="true"
|
||||
checked[*self.checked()]
|
||||
autofocus[*self.autofocus()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()];
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-check-label" for=(&checkbox_id) {
|
||||
(label)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Crea una casilla de verificación estándar.
|
||||
pub fn check() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un interruptor de encendido/apagado (*toggle switch*).
|
||||
pub fn switch() -> Self {
|
||||
Self {
|
||||
checkbox_kind: form::CheckboxKind::Switch,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Checkbox BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del control.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del control.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la variante visual del control.
|
||||
#[builder_fn]
|
||||
pub fn with_kind(mut self, kind: form::CheckboxKind) -> Self {
|
||||
self.checkbox_kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Si se omite, se asigna un identificador generado automáticamente. Para deserializar el campo
|
||||
/// en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del control (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control debe aparecer marcado/activo por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control se muestra en línea con otros controles.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor, lo que permite
|
||||
/// alinear varios controles horizontalmente.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control y su etiqueta se justifican a la derecha del contenedor.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-reverse` al contenedor.
|
||||
#[builder_fn]
|
||||
pub fn with_reverse(mut self, reverse: bool) -> Self {
|
||||
self.reverse = reverse;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
|
||||
/// Componente para crear un **formulario**.
|
||||
///
|
||||
/// Este componente renderiza un `<form>` estándar con soporte para los atributos más habituales:
|
||||
///
|
||||
/// - `id`: identificador opcional del formulario.
|
||||
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
||||
/// - `action`: URL/ruta de destino para el envío.
|
||||
/// - `method`: método usado por el formulario para el envío de los datos (ver explicaciones en
|
||||
/// [`form::Method`](crate::theme::form::Method)).
|
||||
/// - `accept-charset`: juego de caracteres aceptado (por defecto es `"UTF-8"`).
|
||||
/// - `children`: contenido del formulario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let search = Form::new()
|
||||
/// .with_id("search")
|
||||
/// .with_action("/search")
|
||||
/// .with_method(form::Method::Get)
|
||||
/// .with_child(form::input::Field::search().with_name("q"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Form {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del formulario.
|
||||
classes: Classes,
|
||||
/// Devuelve la URL/ruta de destino del formulario.
|
||||
action: AttrValue,
|
||||
/// Devuelve el método para enviar el formulario.
|
||||
method: form::Method,
|
||||
/// Devuelve el juego de caracteres aceptado por el formulario.
|
||||
#[default(_code = "AttrValue::new(\"UTF-8\")")]
|
||||
charset: AttrValue,
|
||||
/// Devuelve la lista de componentes del formulario.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Form {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let method = match self.method() {
|
||||
form::Method::Post => Some("post"),
|
||||
form::Method::Get => None,
|
||||
};
|
||||
Ok(html! {
|
||||
form
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
action=[self.action().get()]
|
||||
method=[method]
|
||||
accept-charset=[self.charset().get()]
|
||||
{
|
||||
(self.children().render(cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Form {
|
||||
// **< Form BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la URL/ruta de destino del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_action(mut self, action: impl AsRef<str>) -> Self {
|
||||
self.action.alter_str(action);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el método para enviar el formulario.
|
||||
///
|
||||
/// - `GET`: el atributo `method` se omite.
|
||||
/// - `POST`: se establece `method="post"`.
|
||||
#[builder_fn]
|
||||
pub fn with_method(mut self, method: form::Method) -> Self {
|
||||
self.method = method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el juego de caracteres aceptado por el formulario.
|
||||
///
|
||||
/// Por defecto se utiliza `"UTF-8"`.
|
||||
#[builder_fn]
|
||||
pub fn with_charset(mut self, charset: impl AsRef<str>) -> Self {
|
||||
self.charset.alter_str(charset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al formulario o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **grupo de controles relacionados** en un formulario.
|
||||
///
|
||||
/// Renderiza un `<fieldset>` con una leyenda opcional que sirve de encabezado y una descripción
|
||||
/// también opcional que aparece justo antes de los controles. Es un elemento semántico que mejora
|
||||
/// la accesibilidad porque los lectores de pantalla anuncian la leyenda antes de leer cada control
|
||||
/// del contenido.
|
||||
///
|
||||
/// Los componentes del grupo se añaden con [`with_child()`](Fieldset::with_child). Si no hay
|
||||
/// contenido para renderizar, el `fieldset` no se genera. Si está deshabilitado, todos sus
|
||||
/// controles hijos quedan deshabilitados automáticamente por el navegador.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let personal_data = form::Fieldset::new()
|
||||
/// .with_legend(L10n::n("Personal data"))
|
||||
/// .with_description(L10n::n("Enter your full name and contact email."))
|
||||
/// .with_child(form::input::Field::text().with_name("name").with_label(L10n::n("Full name")))
|
||||
/// .with_child(form::input::Field::email().with_name("email").with_label(L10n::n("Email")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Fieldset {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del `fieldset`.
|
||||
classes: Classes,
|
||||
/// Devuelve la leyenda del `fieldset`.
|
||||
legend: Attr<L10n>,
|
||||
/// Devuelve la descripción del `fieldset`.
|
||||
description: Attr<L10n>,
|
||||
/// Devuelve si el `fieldset` está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve la lista de componentes del `fieldset`.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Fieldset {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let children = self.children().render(cx);
|
||||
|
||||
if children.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
Ok(html! {
|
||||
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
||||
@if let Some(legend) = self.legend().lookup(cx) {
|
||||
legend { (legend) }
|
||||
}
|
||||
@if let Some(description) = self.description().lookup(cx) {
|
||||
p class="fieldset-description" { (description) }
|
||||
}
|
||||
(children)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Fieldset {
|
||||
// **< Fieldset BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del `fieldset` (grupo de controles).
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al `fieldset`.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_legend(mut self, legend: impl Into<Option<L10n>>) -> Self {
|
||||
self.legend.alter_opt(legend.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la descripción del `fieldset` (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_description(mut self, description: impl Into<Option<L10n>>) -> Self {
|
||||
self.description.alter_opt(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el `fieldset` está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al `fieldset`, o aplica una operación [`ChildOp`] sobre la lista
|
||||
/// de componentes (`children`).
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **campo oculto** del formulario.
|
||||
///
|
||||
/// Renderiza un campo sin ningún marcado visible. Su valor se envía al servidor junto con el resto
|
||||
/// del formulario, pero el usuario no puede verlo ni modificarlo.
|
||||
///
|
||||
/// Es útil para transportar datos de estado, tokens CSRF, identificadores o cualquier valor que
|
||||
/// deba incluirse en el envío sin ser accesible al usuario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let token = form::Hidden::new()
|
||||
/// .with_name("csrf_token")
|
||||
/// .with_value("a1b2c3d4e5");
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. En el servidor se deserializa
|
||||
/// como `String`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// csrf_token: String,
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Hidden {
|
||||
/// Devuelve el nombre del campo oculto.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor del campo oculto.
|
||||
value: AttrValue,
|
||||
}
|
||||
|
||||
impl Component for Hidden {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
input
|
||||
type="hidden"
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()];
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Hidden {
|
||||
// **< Hidden BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el nombre del campo oculto (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor del campo oculto (atributo `value`).
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,453 +0,0 @@
|
|||
//! Definiciones para crear campos de texto de una línea.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// **< Kind >***************************************************************************************
|
||||
|
||||
/// Tipo de campo para un [`form::input::Field`].
|
||||
///
|
||||
/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar
|
||||
/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text),
|
||||
/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email),
|
||||
/// [`telephone()`](Field::telephone) o [`url()`](Field::url).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Kind {
|
||||
/// Entrada de texto genérico (`type="text"`). Es el tipo por defecto.
|
||||
#[default]
|
||||
Text,
|
||||
/// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado.
|
||||
Password,
|
||||
/// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda.
|
||||
Search,
|
||||
/// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo.
|
||||
Email,
|
||||
/// Entrada de un teléfono (`type="tel"`). Activa el teclado de llamadas en móviles.
|
||||
Telephone,
|
||||
/// Entrada de una URL (`type="url"`). Comprueba que la entrada sea una URL bien formada.
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for Kind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Kind::Text => "text",
|
||||
Kind::Password => "password",
|
||||
Kind::Search => "search",
|
||||
Kind::Email => "email",
|
||||
Kind::Telephone => "tel",
|
||||
Kind::Url => "url",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Mode >***************************************************************************************
|
||||
|
||||
/// Sugerencia para el teclado virtual de un [`form::input::Field`].
|
||||
///
|
||||
/// Indica al navegador qué tipo de teclado virtual mostrar en dispositivos móviles o táctiles al
|
||||
/// editar el campo. A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los
|
||||
/// valores aceptados ni activa la validación del navegador; es sólo una sugerencia de presentación.
|
||||
///
|
||||
/// Se establece con [`form::input::Field::with_inputmode()`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Mode {
|
||||
/// Suprime el teclado virtual. Útil en campos con teclado personalizado basado en JavaScript.
|
||||
None,
|
||||
/// Teclado de texto genérico.
|
||||
Text,
|
||||
/// Teclado decimal, con dígitos y separador decimal.
|
||||
Decimal,
|
||||
/// Teclado numérico, con sólo dígitos.
|
||||
Numeric,
|
||||
/// Teclado de teléfono, con dígitos y símbolos `+`, `*` y `#`.
|
||||
Tel,
|
||||
/// Teclado optimizado para búsquedas (puede incluir tecla de búsqueda).
|
||||
Search,
|
||||
/// Teclado optimizado para correo electrónico (incluye `@` y `.`).
|
||||
Email,
|
||||
/// Teclado optimizado para URL (incluye `/`, `.` y `.com`).
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Mode::None => "none",
|
||||
Mode::Text => "text",
|
||||
Mode::Decimal => "decimal",
|
||||
Mode::Numeric => "numeric",
|
||||
Mode::Tel => "tel",
|
||||
Mode::Search => "search",
|
||||
Mode::Email => "email",
|
||||
Mode::Url => "url",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **campo de texto de una línea**.
|
||||
///
|
||||
/// Renderiza los tipos más habituales en formularios:
|
||||
///
|
||||
/// - [`form::input::Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
||||
/// - [`form::input::Field::password()`]: contraseña (`type="password"`).
|
||||
/// - [`form::input::Field::search()`]: búsqueda (`type="search"`).
|
||||
/// - [`form::input::Field::email()`]: correo electrónico (`type="email"`).
|
||||
/// - [`form::input::Field::telephone()`]: teléfono (`type="tel"`).
|
||||
/// - [`form::input::Field::url()`]: URL (`type="url"`).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let email = form::input::Field::email()
|
||||
/// .with_name("email")
|
||||
/// .with_label(L10n::n("Email address"))
|
||||
/// .with_placeholder(L10n::n("user@example.com"))
|
||||
/// .with_autocomplete(Some(form::Autocomplete::email()))
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un campo de texto siempre envía su
|
||||
/// valor, incluso si está vacío. En el servidor se deserializa como `String`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// email: String, // Siempre presente; cadena vacía si el usuario no escribió nada.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del campo.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de campo.
|
||||
kind: Kind,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor inicial del campo.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||
floating_label: bool,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve la longitud mínima permitida en caracteres.
|
||||
minlength: Attr<u16>,
|
||||
/// Devuelve la longitud máxima permitida en caracteres.
|
||||
maxlength: Attr<u16>,
|
||||
/// Devuelve el texto indicativo del campo.
|
||||
placeholder: Attr<L10n>,
|
||||
/// Devuelve la configuración de autocompletado del campo.
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
/// Devuelve si el campo recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el campo es de sólo lectura.
|
||||
readonly: bool,
|
||||
/// Devuelve si el campo es obligatorio.
|
||||
required: bool,
|
||||
/// Devuelve si el campo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si el campo se muestra como texto plano sin bordes ni fondo.
|
||||
plaintext: bool,
|
||||
/// Devuelve la sugerencia de teclado virtual para el campo.
|
||||
inputmode: Attr<Mode>,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if *self.floating_label() {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||
}
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
util::join!("form-field form-field-", self.kind().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
||||
let input_class = if *self.plaintext() {
|
||||
"form-control-plaintext"
|
||||
} else {
|
||||
"form-control"
|
||||
};
|
||||
// La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo
|
||||
// está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`.
|
||||
let placeholder = if *self.floating_label() {
|
||||
Some(self.placeholder().lookup(cx).unwrap_or_default())
|
||||
} else {
|
||||
self.placeholder().lookup(cx)
|
||||
};
|
||||
let label = match self.label().lookup(cx) {
|
||||
Some(text) => html! {
|
||||
label for=[input_id.as_deref()] class="form-label" {
|
||||
(text)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => html! {},
|
||||
};
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if !*self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
input
|
||||
type=(self.kind())
|
||||
id=[input_id.as_deref()]
|
||||
class=(input_class)
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()]
|
||||
minlength=[self.minlength().get()]
|
||||
maxlength=[self.maxlength().get()]
|
||||
placeholder=[placeholder]
|
||||
inputmode=[self.inputmode().get()]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
readonly[*self.readonly() || *self.plaintext()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()];
|
||||
@if *self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
/// Crea un campo de **texto genérico** (`type="text"`).
|
||||
///
|
||||
/// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada
|
||||
/// textual sin restricciones de formato específicas.
|
||||
pub fn text() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un campo de **contraseña** (`type="password"`).
|
||||
///
|
||||
/// El navegador oculta los caracteres introducidos. Se recomienda usar con
|
||||
/// [`with_autocomplete()`](Self::with_autocomplete) para permitir autorrellenar con una
|
||||
/// contraseña guardada o dejar al usuario recibir sugerencias o crear una nueva.
|
||||
pub fn password() -> Self {
|
||||
Self {
|
||||
kind: Kind::Password,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **búsqueda** (`type="search"`).
|
||||
///
|
||||
/// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos
|
||||
/// navegadores añaden un botón para borrar el contenido.
|
||||
pub fn search() -> Self {
|
||||
Self {
|
||||
kind: Kind::Search,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **correo electrónico** (`type="email"`).
|
||||
///
|
||||
/// El navegador valida el formato de la dirección antes de enviar el formulario. En
|
||||
/// dispositivos móviles muestra un teclado adaptado para introducir direcciones de correo.
|
||||
pub fn email() -> Self {
|
||||
Self {
|
||||
kind: Kind::Email,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **teléfono** (`type="tel"`).
|
||||
///
|
||||
/// No impone ninguna restricción de formato (los formatos de teléfono varían por país), pero
|
||||
/// en dispositivos móviles muestra el teclado numérico de llamadas.
|
||||
pub fn telephone() -> Self {
|
||||
Self {
|
||||
kind: Kind::Telephone,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **URL** (`type="url"`).
|
||||
///
|
||||
/// El navegador valida que el valor sea una URL bien formada antes de enviar el formulario.
|
||||
pub fn url() -> Self {
|
||||
Self {
|
||||
kind: Kind::Url,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||
///
|
||||
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
|
||||
/// contenido.
|
||||
#[builder_fn]
|
||||
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||
self.floating_label = floating_label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo).
|
||||
#[builder_fn]
|
||||
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self {
|
||||
self.minlength.alter_opt(minlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite).
|
||||
#[builder_fn]
|
||||
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self {
|
||||
self.maxlength.alter_opt(maxlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto indicativo del campo (`None` para quitarlo).
|
||||
///
|
||||
/// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir.
|
||||
/// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo.
|
||||
#[builder_fn]
|
||||
pub fn with_placeholder(mut self, placeholder: impl Into<Option<L10n>>) -> Self {
|
||||
self.placeholder.alter_opt(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la configuración de autocompletado del campo.
|
||||
///
|
||||
/// Usar los métodos de [`form::Autocomplete`] para los valores más habituales (p. ej.
|
||||
/// [`Autocomplete::email()`](form::Autocomplete::email) o
|
||||
/// [`Autocomplete::current_password()`](form::Autocomplete::current_password)).
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es de sólo lectura.
|
||||
#[builder_fn]
|
||||
pub fn with_readonly(mut self, readonly: bool) -> Self {
|
||||
self.readonly = readonly;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo se muestra como texto plano (sin bordes ni fondo).
|
||||
///
|
||||
/// Útil para mostrar un valor no editable en pantalla que sí se envía al servidor con el
|
||||
/// formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_plaintext(mut self, plaintext: bool) -> Self {
|
||||
self.plaintext = plaintext;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el modo de entrada sugerido para el teclado virtual en dispositivos móviles.
|
||||
///
|
||||
/// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados
|
||||
/// ni activa la validación del navegador; es sólo una sugerencia de presentación.
|
||||
#[builder_fn]
|
||||
pub fn with_inputmode(mut self, inputmode: Option<Mode>) -> Self {
|
||||
self.inputmode.alter_opt(inputmode);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
// **< CheckboxKind >*******************************************************************************
|
||||
|
||||
/// Variante visual para [`form::Checkbox`](crate::theme::form::Checkbox) en un formulario.
|
||||
///
|
||||
/// Determina si el control se renderiza como una casilla de verificación estándar o como un
|
||||
/// interruptor (*toggle switch*).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum CheckboxKind {
|
||||
/// Casilla de verificación estándar. Es el tipo por defecto.
|
||||
#[default]
|
||||
Check,
|
||||
/// Interruptor de encendido/apagado.
|
||||
Switch,
|
||||
// TODO: Añadir variante `NativeSwitch` cuando el atributo `switch` de la propuesta WHATWG
|
||||
// (https://github.com/whatwg/html/issues/9546) sea estándar y tenga soporte amplio. Safari ya
|
||||
// lo soporta. También se añadiría el constructor `Checkbox::native_switch()`.
|
||||
}
|
||||
|
||||
// **< Autocomplete / AutofillField >***************************************************************
|
||||
|
||||
/// Configuración para el autocompletado de controles en un formulario.
|
||||
///
|
||||
/// Indica al navegador si puede sugerir o rellenar automáticamente el valor del control usando
|
||||
/// datos que el usuario haya introducido antes (credenciales guardadas, datos de contacto, etc.).
|
||||
///
|
||||
/// Lo habitual es usar uno de los **métodos predefinidos**, que generan el token canónico adecuado
|
||||
/// para cada tipo de dato:
|
||||
///
|
||||
/// - Identidad y credenciales: [`username()`](Autocomplete::username),
|
||||
/// [`email()`](Autocomplete::email), [`current_password()`](Autocomplete::current_password),
|
||||
/// [`new_password()`](Autocomplete::new_password), [`otp()`](Autocomplete::otp).
|
||||
/// - Token o tokens directos: [`token(field)`](Autocomplete::token) con una variante de
|
||||
/// [`AutofillField`].
|
||||
/// - Direcciones: [`shipping(field)`](Autocomplete::shipping),
|
||||
/// [`billing(field)`](Autocomplete::billing).
|
||||
/// - Datos de contacto: [`home(field)`](Autocomplete::home), [`work(field)`](Autocomplete::work),
|
||||
/// [`mobile(field)`](Autocomplete::mobile), [`fax(field)`](Autocomplete::fax),
|
||||
/// [`pager(field)`](Autocomplete::pager).
|
||||
/// - Sección personalizada: [`section(name, field)`](Autocomplete::section).
|
||||
///
|
||||
/// Para activar o inhibir el autocompletado sin especificar el tipo de dato basta con usar las
|
||||
/// variantes [`form::Autocomplete::On`](Autocomplete::On) o
|
||||
/// [`form::Autocomplete::Off`](Autocomplete::Off). Para combinaciones no cubiertas por los métodos
|
||||
/// anteriores, [`custom()`](Autocomplete::custom) acepta cualquier cadena ASCII válida.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Correo electrónico con sugerencia semántica del navegador.
|
||||
/// let ac = form::Autocomplete::email();
|
||||
///
|
||||
/// // Contraseña nueva en un formulario de registro.
|
||||
/// let ac = form::Autocomplete::new_password();
|
||||
///
|
||||
/// // Teléfono de contacto del trabajo.
|
||||
/// let ac = form::Autocomplete::work(form::AutofillField::Tel);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Autocomplete {
|
||||
/// Genera `autocomplete="on"`.
|
||||
On,
|
||||
/// Genera `autocomplete="off"`.
|
||||
Off,
|
||||
/// Contiene el valor literal del atributo `autocomplete` tal como se enviará al navegador.
|
||||
///
|
||||
/// Debe contener un token o lista de tokens separados por espacios (p. ej. `"username"` o
|
||||
/// `"username webauthn"`).
|
||||
Custom(CowStr),
|
||||
}
|
||||
|
||||
impl Autocomplete {
|
||||
// --< Token >----------------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete` a partir del token o tokens del [`AutofillField`] indicado.
|
||||
#[inline]
|
||||
pub fn token(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Borrowed(field.as_str()))
|
||||
}
|
||||
|
||||
// --< Secciones >------------------------------------------------------------------------------
|
||||
|
||||
/// Construye `autocomplete` con un prefijo de sección y un token o tokens del
|
||||
/// [`form::AutofillField`](AutofillField) indicado.
|
||||
///
|
||||
/// Genera `autocomplete="section-<name> <field>"`. Si `name` no es ASCII o contiene espacios,
|
||||
/// se ignora la sección y se genera sólo el token indicado.
|
||||
///
|
||||
/// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma
|
||||
/// página (p. ej. una dirección de envío y otra de facturación).
|
||||
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
|
||||
match util::normalize_ascii(name.as_ref()) {
|
||||
Ok(n) if !n.as_ref().contains(' ') => {
|
||||
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
|
||||
}
|
||||
_ => Self::token(field),
|
||||
}
|
||||
}
|
||||
|
||||
// --< Comunes >--------------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete="username"`.
|
||||
pub fn username() -> Self {
|
||||
Self::token(AutofillField::Username)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn username_webauthn() -> Self {
|
||||
Self::custom("username webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="email"`.
|
||||
pub fn email() -> Self {
|
||||
Self::token(AutofillField::Email)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password"`.
|
||||
pub fn current_password() -> Self {
|
||||
Self::token(AutofillField::CurrentPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn current_password_webauthn() -> Self {
|
||||
Self::custom("current-password webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="new-password"`.
|
||||
pub fn new_password() -> Self {
|
||||
Self::token(AutofillField::NewPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="one-time-code"`.
|
||||
pub fn otp() -> Self {
|
||||
Self::token(AutofillField::OneTimeCode)
|
||||
}
|
||||
|
||||
// --< Direcciones >----------------------------------------------------------------------------
|
||||
|
||||
/// Contexto de dirección de envío. Genera `autocomplete="shipping <field>"`.
|
||||
pub fn shipping(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("shipping ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Contexto de dirección de facturación. Genera `autocomplete="billing <field>"`.
|
||||
pub fn billing(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("billing ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Contacto >-------------------------------------------------------------------------------
|
||||
|
||||
/// Detalle de contacto: `autocomplete="home <field>"`.
|
||||
pub fn home(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("home ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="work <field>"`.
|
||||
pub fn work(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("work ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="mobile <field>"`.
|
||||
pub fn mobile(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("mobile ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="fax <field>"`.
|
||||
pub fn fax(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("fax ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="pager <field>"`.
|
||||
pub fn pager(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("pager ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Tokens personalizados >------------------------------------------------------------------
|
||||
|
||||
/// Crea un valor de `autocomplete` a partir de una cadena de texto libre.
|
||||
///
|
||||
/// Normaliza la entrada recortando espacios extra, compactando separadores y convirtiendo a
|
||||
/// minúsculas. Si el resultado es `"on"` u `"off"`, devuelve la variante correspondiente; si la
|
||||
/// entrada contiene caracteres no ASCII o queda vacía tras normalizar, devuelve
|
||||
/// [`form::Autocomplete::On`](Autocomplete::On).
|
||||
///
|
||||
/// Para los casos habituales se recomienda usar los métodos predefinidos de
|
||||
/// [`form::Autocomplete`](Autocomplete).
|
||||
pub fn custom(autocomplete: impl Into<CowStr>) -> Self {
|
||||
let value: CowStr = autocomplete.into();
|
||||
let raw = value.as_ref();
|
||||
|
||||
// Normaliza la entrada.
|
||||
let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else {
|
||||
return Self::On;
|
||||
};
|
||||
let autocomplete = normalized.as_ref();
|
||||
|
||||
// Identifica valores especiales.
|
||||
if autocomplete == "on" {
|
||||
return Self::On;
|
||||
}
|
||||
if autocomplete == "off" {
|
||||
return Self::Off;
|
||||
}
|
||||
|
||||
// Mantiene el `Cow` original si no cambia nada (no reserva espacio).
|
||||
if autocomplete == raw {
|
||||
return Self::Custom(value);
|
||||
}
|
||||
// En otro caso asigna espacio para la normalización.
|
||||
Self::Custom(Cow::Owned(normalized.into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Autocomplete {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Autocomplete::On => f.write_str("on"),
|
||||
Autocomplete::Off => f.write_str("off"),
|
||||
Autocomplete::Custom(c) => f.write_str(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokens para el autocompletado de formularios con [`form::Autocomplete`](Autocomplete).
|
||||
///
|
||||
/// Representa los tokens de autorrelleno (*autofill field*) definidos por la
|
||||
/// [especificación WHATWG](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field)
|
||||
/// para el atributo `autocomplete`. Cada variante corresponde exactamente a un token canónico
|
||||
/// de dicha especificación.
|
||||
///
|
||||
/// Los valores se usan en combinación con [`form::Autocomplete`](Autocomplete) para construir el
|
||||
/// valor completo del atributo `autocomplete` de un control de formulario. Los métodos de
|
||||
/// [`form::Autocomplete`](Autocomplete) como [`token()`](Autocomplete::token),
|
||||
/// [`email()`](Autocomplete::email), [`shipping()`](Autocomplete::shipping) o
|
||||
/// [`section()`](Autocomplete::section) aceptan variantes de `AutofillField` para generar el token
|
||||
/// correspondiente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
|
||||
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
|
||||
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutofillField {
|
||||
// --< Identidad / cuenta >---------------------------------------------------------------------
|
||||
/// Nombre completo.
|
||||
Name,
|
||||
/// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra.").
|
||||
HonorificPrefix,
|
||||
/// Nombre de pila.
|
||||
GivenName,
|
||||
/// Nombre adicional (p. ej. segundo nombre).
|
||||
AdditionalName,
|
||||
/// Apellidos.
|
||||
FamilyName,
|
||||
/// Sufijo honorífico (p. ej. "Jr.", "PhD").
|
||||
HonorificSuffix,
|
||||
/// Apodo.
|
||||
Nickname,
|
||||
/// Identificador de usuario (login).
|
||||
Username,
|
||||
|
||||
// --< Credenciales >---------------------------------------------------------------------------
|
||||
/// Contraseña actual.
|
||||
CurrentPassword,
|
||||
/// Nueva contraseña.
|
||||
NewPassword,
|
||||
/// Código de un solo uso (OTP).
|
||||
OneTimeCode,
|
||||
|
||||
// --< Organización >---------------------------------------------------------------------------
|
||||
/// Cargo o título dentro de una organización.
|
||||
OrganizationTitle,
|
||||
/// Nombre de la organización.
|
||||
Organization,
|
||||
|
||||
// --< Contacto >-------------------------------------------------------------------------------
|
||||
/// Correo electrónico.
|
||||
Email,
|
||||
/// Teléfono.
|
||||
Tel,
|
||||
/// Prefijo/código de país del teléfono (incluye `+`).
|
||||
TelCountryCode,
|
||||
/// Teléfono sin el código de país.
|
||||
TelNational,
|
||||
/// Código de área (si aplica).
|
||||
TelAreaCode,
|
||||
/// Teléfono sin código de país ni de área.
|
||||
TelLocal,
|
||||
/// Prefijo local (primera parte tras el área).
|
||||
TelLocalPrefix,
|
||||
/// Sufijo local (segunda parte tras el área).
|
||||
TelLocalSuffix,
|
||||
/// Extensión interna.
|
||||
TelExtension,
|
||||
/// URL personal o de contacto.
|
||||
Url,
|
||||
/// Referencia de mensajería instantánea (URL).
|
||||
Impp,
|
||||
|
||||
// --< Dirección >------------------------------------------------------------------------------
|
||||
/// Dirección postal completa (una sola línea/textarea).
|
||||
StreetAddress,
|
||||
/// Línea 1 de dirección.
|
||||
AddressLine1,
|
||||
/// Línea 2 de dirección.
|
||||
AddressLine2,
|
||||
/// Línea 3 de dirección.
|
||||
AddressLine3,
|
||||
/// Nivel administrativo 4 (el más específico).
|
||||
AddressLevel4,
|
||||
/// Nivel administrativo 3.
|
||||
AddressLevel3,
|
||||
/// Nivel administrativo 2 (p. ej. ciudad/municipio).
|
||||
AddressLevel2,
|
||||
/// Nivel administrativo 1 (p. ej. provincia/estado).
|
||||
AddressLevel1,
|
||||
/// Código postal.
|
||||
PostalCode,
|
||||
/// País (el navegador rellena el código de país).
|
||||
Country,
|
||||
/// Nombre del país.
|
||||
CountryName,
|
||||
|
||||
// --< Pago >-----------------------------------------------------------------------------------
|
||||
/// Nombre del titular de la tarjeta.
|
||||
CcName,
|
||||
/// Nombre de pila del titular de la tarjeta.
|
||||
CcGivenName,
|
||||
/// Nombre adicional del titular de la tarjeta.
|
||||
CcAdditionalName,
|
||||
/// Apellidos del titular de la tarjeta.
|
||||
CcFamilyName,
|
||||
/// Número de tarjeta.
|
||||
CcNumber,
|
||||
/// Fecha de caducidad (completa).
|
||||
CcExp,
|
||||
/// Mes de caducidad.
|
||||
CcExpMonth,
|
||||
/// Año de caducidad.
|
||||
CcExpYear,
|
||||
/// Código de seguridad (CVC/CVV).
|
||||
CcCsc,
|
||||
/// Tipo de tarjeta (p. ej. visa/mastercard).
|
||||
CcType,
|
||||
|
||||
// --< Transacción / preferencias >-------------------------------------------------------------
|
||||
/// Moneda preferida para la transacción (código ISO 4217).
|
||||
TransactionCurrency,
|
||||
/// Cantidad de la transacción (número).
|
||||
TransactionAmount,
|
||||
/// Idioma preferido (BCP 47).
|
||||
Language,
|
||||
|
||||
// --< Datos personales >-----------------------------------------------------------------------
|
||||
/// Fecha de nacimiento completa.
|
||||
Bday,
|
||||
/// Día de nacimiento.
|
||||
BdayDay,
|
||||
/// Mes de nacimiento.
|
||||
BdayMonth,
|
||||
/// Año de nacimiento.
|
||||
BdayYear,
|
||||
/// Sexo (valor libre guardado por el navegador).
|
||||
Sex,
|
||||
/// Foto (URL o referencia guardada por el navegador).
|
||||
Photo,
|
||||
}
|
||||
|
||||
impl AutofillField {
|
||||
/// Devuelve el token exacto definido por HTML para `autocomplete`.
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AutofillField::Name => "name",
|
||||
AutofillField::HonorificPrefix => "honorific-prefix",
|
||||
AutofillField::GivenName => "given-name",
|
||||
AutofillField::AdditionalName => "additional-name",
|
||||
AutofillField::FamilyName => "family-name",
|
||||
AutofillField::HonorificSuffix => "honorific-suffix",
|
||||
AutofillField::Nickname => "nickname",
|
||||
AutofillField::Username => "username",
|
||||
|
||||
AutofillField::CurrentPassword => "current-password",
|
||||
AutofillField::NewPassword => "new-password",
|
||||
AutofillField::OneTimeCode => "one-time-code",
|
||||
|
||||
AutofillField::OrganizationTitle => "organization-title",
|
||||
AutofillField::Organization => "organization",
|
||||
|
||||
AutofillField::Email => "email",
|
||||
AutofillField::Tel => "tel",
|
||||
AutofillField::TelCountryCode => "tel-country-code",
|
||||
AutofillField::TelNational => "tel-national",
|
||||
AutofillField::TelAreaCode => "tel-area-code",
|
||||
AutofillField::TelLocal => "tel-local",
|
||||
AutofillField::TelLocalPrefix => "tel-local-prefix",
|
||||
AutofillField::TelLocalSuffix => "tel-local-suffix",
|
||||
AutofillField::TelExtension => "tel-extension",
|
||||
AutofillField::Url => "url",
|
||||
AutofillField::Impp => "impp",
|
||||
|
||||
AutofillField::StreetAddress => "street-address",
|
||||
AutofillField::AddressLine1 => "address-line1",
|
||||
AutofillField::AddressLine2 => "address-line2",
|
||||
AutofillField::AddressLine3 => "address-line3",
|
||||
AutofillField::AddressLevel4 => "address-level4",
|
||||
AutofillField::AddressLevel3 => "address-level3",
|
||||
AutofillField::AddressLevel2 => "address-level2",
|
||||
AutofillField::AddressLevel1 => "address-level1",
|
||||
AutofillField::PostalCode => "postal-code",
|
||||
AutofillField::Country => "country",
|
||||
AutofillField::CountryName => "country-name",
|
||||
|
||||
AutofillField::CcName => "cc-name",
|
||||
AutofillField::CcGivenName => "cc-given-name",
|
||||
AutofillField::CcAdditionalName => "cc-additional-name",
|
||||
AutofillField::CcFamilyName => "cc-family-name",
|
||||
AutofillField::CcNumber => "cc-number",
|
||||
AutofillField::CcExp => "cc-exp",
|
||||
AutofillField::CcExpMonth => "cc-exp-month",
|
||||
AutofillField::CcExpYear => "cc-exp-year",
|
||||
AutofillField::CcCsc => "cc-csc",
|
||||
AutofillField::CcType => "cc-type",
|
||||
|
||||
AutofillField::TransactionCurrency => "transaction-currency",
|
||||
AutofillField::TransactionAmount => "transaction-amount",
|
||||
AutofillField::Language => "language",
|
||||
|
||||
AutofillField::Bday => "bday",
|
||||
AutofillField::BdayDay => "bday-day",
|
||||
AutofillField::BdayMonth => "bday-month",
|
||||
AutofillField::BdayYear => "bday-year",
|
||||
AutofillField::Sex => "sex",
|
||||
AutofillField::Photo => "photo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Method >*************************************************************************************
|
||||
|
||||
/// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos.
|
||||
///
|
||||
/// En HTML, el atributo `method` del formulario indica **cómo** se envían los datos:
|
||||
///
|
||||
/// - **GET**: los pares `name=value` se codifican en la **URL** añadiendo una cadena de consulta
|
||||
/// como `?a=1&b=2`. Es el método por defecto en HTML cuando no se especifica. Suele ser apropiado
|
||||
/// para **búsquedas** o formularios que no modifican datos ni el estado del sistema.
|
||||
///
|
||||
/// - **POST**: los datos se envían en el **cuerpo** de la petición (*request body*). Es apropiado
|
||||
/// para acciones que **modifican el estado** o cuando hay formularios grandes. Es el **método por
|
||||
/// defecto** en PageTop.
|
||||
///
|
||||
/// # Consideraciones prácticas
|
||||
///
|
||||
/// - **Visibilidad y privacidad**: con GET los datos quedan visibles en la URL (historial, *logs*,
|
||||
/// marcadores). No se recomienda para datos sensibles. Con POST no van en la URL, pero **no se
|
||||
/// cifran** por sí mismos; por eso es esencial el uso de HTTPS.
|
||||
/// - **Tamaño**: GET está limitado por la longitud máxima de URL que acepten el navegador y el
|
||||
/// servidor. POST es más flexible para cargas grandes.
|
||||
/// - **Ficheros**: la subida de ficheros requiere `method="post"` y un `enctype` adecuado
|
||||
/// (habitualmente `multipart/form-data`).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Method {
|
||||
/// Envía los datos en el cuerpo de la petición.
|
||||
///
|
||||
/// Es el **método por defecto** en PageTop. Recomendado para operaciones que modifican el
|
||||
/// estado o para envíos grandes.
|
||||
#[default]
|
||||
Post,
|
||||
|
||||
/// Envía los datos en la URL como una cadena *query*.
|
||||
///
|
||||
/// Recomendado para búsquedas y operaciones que no modifican datos ni el estado del sistema.
|
||||
Get,
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
//! Definiciones para crear grupos de botones de opción (*radio buttons*).
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Botón de opción individual de un [`form::radio::Field`](Field).
|
||||
///
|
||||
/// Representa cada opción de un grupo de opciones exclusivas entre sí, con un valor (el que se
|
||||
/// envía al servidor), una etiqueta localizable visible y puede marcarse como seleccionada o
|
||||
/// inicialmente deshabilitada de forma independiente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el valor enviado al servidor cuando la opción está seleccionada.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta de la opción.
|
||||
label: L10n,
|
||||
/// Devuelve si la opción debe aparecer seleccionada por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si la opción está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea una nueva opción con el valor y la etiqueta indicados.
|
||||
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
value: AttrValue::new(value),
|
||||
label,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si la opción aparece seleccionada por defecto.
|
||||
///
|
||||
/// Si varias opciones del grupo tienen `checked` activo, sólo la primera se renderizará como
|
||||
/// seleccionada; las demás se ignorarán.
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la opción está inicialmente deshabilitada.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **grupo de botones de opción**.
|
||||
///
|
||||
/// Renderiza un grupo de botones de opción [`form::radio::Item`](Item) que comparten el mismo
|
||||
/// atributo `name`, por lo que sólo puede seleccionarse uno a la vez. Las opciones se añaden con
|
||||
/// [`with_item()`](Field::with_item).
|
||||
///
|
||||
/// Si se activa el modo en línea [`with_inline()`](Field::with_inline), los botones se disponen
|
||||
/// horizontalmente. El atributo `required` se propaga a todos los botones del grupo para cumplir
|
||||
/// con la especificación HTML.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let plan = form::radio::Field::new()
|
||||
/// .with_name("plan")
|
||||
/// .with_label(L10n::n("Subscription plan"))
|
||||
/// .with_item(form::radio::Item::new("monthly", L10n::n("Monthly")))
|
||||
/// .with_item(form::radio::Item::new("annual", L10n::n("Annual")).with_checked(true))
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el usuario selecciona un botón, el navegador envía algo como `plan=monthly`; si no
|
||||
/// selecciona ninguno, no envía nada. En el servidor el campo se deserializa como `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// plan: Option<String>, // Some("monthly"), Some("annual"), ..., o None si no se seleccionó.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del grupo.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre compartido por todos los botones de opción del grupo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del grupo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del grupo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las opciones del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si la selección de alguna opción del grupo es obligatoria.
|
||||
required: bool,
|
||||
/// Devuelve si todo el grupo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si los botones se muestran en línea horizontalmente.
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-radios");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-label" {
|
||||
(label)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@let item_classes = if *self.inline() {
|
||||
"form-check form-check-inline"
|
||||
} else {
|
||||
"form-check"
|
||||
};
|
||||
@let mut do_check = true;
|
||||
@for (item, i) in self.items().iter().zip(1..) {
|
||||
@let checked = {
|
||||
let c = *item.checked() && do_check;
|
||||
if c { do_check = false; }
|
||||
c
|
||||
};
|
||||
@let i = i.to_string();
|
||||
@let item_id = util::join!(&container_id, "-radio-", &i);
|
||||
div class=(item_classes) {
|
||||
input
|
||||
type="radio"
|
||||
id=(&item_id)
|
||||
class="form-check-input"
|
||||
name=(&name)
|
||||
value=[item.value().get()]
|
||||
checked[checked]
|
||||
required[*self.required()]
|
||||
disabled[*item.disabled() || *self.disabled()];
|
||||
label class="form-check-label" for=(&item_id) {
|
||||
(item.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del grupo de opciones.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de opciones.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre compartido por todos los botones de opción del grupo.
|
||||
///
|
||||
/// Todas las opciones [`form::radio::Item`](Item) del grupo llevarán este mismo `name`, lo que
|
||||
/// garantiza la exclusividad de la selección. Es imprescindible establecer un `name`; sin él
|
||||
/// los botones no se envían al servidor.
|
||||
///
|
||||
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
|
||||
/// el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade una opción al grupo. Las opciones se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la selección de alguna opción del grupo es obligatoria.
|
||||
///
|
||||
/// El atributo `required` se propaga a todos los botones del grupo para cumplir con la
|
||||
/// especificación HTML.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si todo el grupo está deshabilitado.
|
||||
///
|
||||
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si los botones se muestran en línea horizontalmente.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada opción.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **control deslizante** de rango.
|
||||
///
|
||||
/// Renderiza una barra deslizante con una etiqueta opcional y un texto de ayuda. Permite
|
||||
/// seleccionar un valor de entre una lista de valores posibles, acotados por un valor mínimo y
|
||||
/// máximo, con un paso opcional entre valores.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let volume = form::Range::new()
|
||||
/// .with_name("volume")
|
||||
/// .with_label(L10n::n("Volume"))
|
||||
/// .with_min(Some(0.0))
|
||||
/// .with_max(Some(100.0))
|
||||
/// .with_step(Some(5.0))
|
||||
/// .with_value(Some(50.0));
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un control deslizante siempre
|
||||
/// envía su valor. En el servidor se deserializa como `f64`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// volume: f64, // Siempre presente con el valor numérico seleccionado.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Range {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del control deslizante.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve el valor mínimo permitido.
|
||||
min: Attr<f64>,
|
||||
/// Devuelve el valor máximo permitido.
|
||||
max: Attr<f64>,
|
||||
/// Devuelve el incremento entre valores del campo.
|
||||
step: Attr<f64>,
|
||||
/// Devuelve el valor inicial del campo.
|
||||
value: Attr<f64>,
|
||||
/// Devuelve si el control recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el control está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Range {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-range");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label for=[range_id.as_deref()] class="form-label" { (label) }
|
||||
}
|
||||
input
|
||||
type="range"
|
||||
id=[range_id.as_deref()]
|
||||
class="form-range"
|
||||
name=[self.name().get()]
|
||||
min=[self.min().get()]
|
||||
max=[self.max().get()]
|
||||
step=[self.step().get()]
|
||||
value=[self.value().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()];
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Range {
|
||||
// **< Range BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del control deslizante.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del control deslizante.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor mínimo del rango.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `min` y deja que el navegador aplique su valor por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_min(mut self, min: Option<f64>) -> Self {
|
||||
self.min.alter_opt(min);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor máximo del rango.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `max` y deja que el navegador aplique su valor por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_max(mut self, max: Option<f64>) -> Self {
|
||||
self.max.alter_opt(max);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el incremento entre valores del campo.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto
|
||||
/// (normalmente `1`).
|
||||
#[builder_fn]
|
||||
pub fn with_step(mut self, step: Option<f64>) -> Self {
|
||||
self.step.alter_opt(step);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del campo.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto
|
||||
/// (normalmente el punto medio del rango).
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: Option<f64>) -> Self {
|
||||
self.value.alter_opt(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,459 +0,0 @@
|
|||
//! Definiciones para crear listas de selección.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Elemento individual de [`form::select::Field`] o de [`form::select::Group`].
|
||||
///
|
||||
/// Representa un elemento dentro de una lista de selección o de un grupo de elementos de la lista.
|
||||
/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el
|
||||
/// usuario.
|
||||
///
|
||||
/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Self::with_selected) o
|
||||
/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Self::with_disabled).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el valor enviado al servidor cuando se selecciona el elemento.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta visible del elemento.
|
||||
label: L10n,
|
||||
/// Devuelve si el elemento debe aparecer seleccionado por defecto.
|
||||
selected: bool,
|
||||
/// Devuelve si el elemento está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un nuevo elemento con el valor y la etiqueta indicados.
|
||||
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
value: AttrValue::new(value),
|
||||
label,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si el elemento aparece seleccionado por defecto.
|
||||
///
|
||||
/// En una lista de selección única, el navegador aplica la selección al último elemento marcado
|
||||
/// si hay más de uno; mientras que en una lista múltiple se respetan todos los elementos
|
||||
/// marcados.
|
||||
pub fn with_selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el elemento está deshabilitado.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Group >**************************************************************************************
|
||||
|
||||
/// Grupo de elementos dentro de [`form::select::Field`].
|
||||
///
|
||||
/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El
|
||||
/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Self::with_disabled).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let group = form::select::Group::new(L10n::n("Europe"))
|
||||
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
|
||||
/// .with_item(form::select::Item::new("fr", L10n::n("French")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Group {
|
||||
/// Devuelve la etiqueta visible del grupo de elementos.
|
||||
label: L10n,
|
||||
/// Devuelve los elementos del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si el grupo de elementos está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Crea un nuevo grupo con la etiqueta indicada.
|
||||
pub fn new(label: L10n) -> Self {
|
||||
Self {
|
||||
label,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Group BUILDER >**************************************************************************
|
||||
|
||||
/// Añade un elemento al grupo. Los elementos se muestran en el orden en que se añaden.
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el grupo de elementos está deshabilitado en bloque.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Entry >**************************************************************************************
|
||||
|
||||
/// Entrada de [`form::select::Field`] con un elemento o un grupo de elementos.
|
||||
///
|
||||
/// Cada entrada se crea implícitamente cuando se usa [`form::select::Field::with_item()`] para
|
||||
/// añadir un elemento individual o [`form::select::Field::with_group()`] para añadir un grupo de
|
||||
/// elementos a una lista de selección.
|
||||
///
|
||||
/// Con [`form::select::Field::entries()`] se pueden recuperar todas las entradas para su
|
||||
/// renderizado.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Entry {
|
||||
/// Elemento individual.
|
||||
Item(Item),
|
||||
/// Grupo de elementos.
|
||||
Group(Group),
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear una **lista de selección**.
|
||||
///
|
||||
/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir
|
||||
/// uno, o más de uno si se activa la selección múltiple con
|
||||
/// [`with_multiple()`](Self::with_multiple).
|
||||
///
|
||||
/// Los elementos individuales se añaden con [`with_item()`](Self::with_item); los grupos de
|
||||
/// elementos con un encabezado común se añaden con [`with_group()`](Self::with_group). Ambos
|
||||
/// métodos pueden combinarse libremente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let idioma = form::select::Field::new()
|
||||
/// .with_name("language")
|
||||
/// .with_label(L10n::n("Language"))
|
||||
/// .with_item(form::select::Item::new("", L10n::n("— Choose —")).with_selected(true))
|
||||
/// .with_group(
|
||||
/// form::select::Group::new(L10n::n("Europe"))
|
||||
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
|
||||
/// .with_item(form::select::Item::new("fr", L10n::n("French"))),
|
||||
/// )
|
||||
/// .with_group(
|
||||
/// form::select::Group::new(L10n::n("Americas"))
|
||||
/// .with_item(form::select::Item::new("en", L10n::n("English")))
|
||||
/// .with_item(form::select::Item::new("pt", L10n::n("Portuguese"))),
|
||||
/// )
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el usuario selecciona un elemento y envía el formulario, el navegador transmite
|
||||
/// `name=valor`. Si el campo es obligatorio el valor siempre estará presente y puede deserializarse
|
||||
/// como `String`; si es opcional, usa `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// language: String, // Siempre presente (campo obligatorio).
|
||||
/// // language: Option<String>, // None si no se selecciona ninguna opción.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Con selección múltiple activa, el navegador envía un valor por cada elemento marcado; si no se
|
||||
/// marca ninguno, no envía nada. Usa `Vec<String>` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// interests: Vec<String>, // p. ej. ["art", "tech"] o [] si no se marcó ninguna.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor de la lista de selección.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||
floating_label: bool,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las entradas de la lista (elementos individuales y grupos de elementos).
|
||||
entries: Vec<Entry>,
|
||||
/// Devuelve si la lista permite selección múltiple.
|
||||
multiple: bool,
|
||||
/// Devuelve el número de filas visibles de la lista de selección.
|
||||
rows: Attr<u16>,
|
||||
/// Devuelve la configuración de autocompletado del campo.
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
/// Devuelve si la lista recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si la selección de un elemento es obligatoria.
|
||||
required: bool,
|
||||
/// Devuelve si la lista está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if *self.floating_label() {
|
||||
self.multiple = false;
|
||||
self.rows.alter_opt(None::<u16>);
|
||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||
}
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-select");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
||||
let label = match self.label().lookup(cx) {
|
||||
Some(text) => html! {
|
||||
label for=[select_id.as_deref()] class="form-label" {
|
||||
(text)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => html! {},
|
||||
};
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if !*self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
select
|
||||
id=[select_id.as_deref()]
|
||||
class="form-select"
|
||||
name=[self.name().get()]
|
||||
multiple[*self.multiple()]
|
||||
size=[self.rows().get()]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@for entry in self.entries() {
|
||||
@match entry {
|
||||
Entry::Item(opt) => {
|
||||
option
|
||||
value=(opt.value().as_str().unwrap_or(""))
|
||||
selected[*opt.selected()]
|
||||
disabled[*opt.disabled()]
|
||||
{
|
||||
(opt.label().using(cx))
|
||||
}
|
||||
}
|
||||
Entry::Group(group) => {
|
||||
optgroup
|
||||
label=(group.label().using(cx))
|
||||
disabled[*group.disabled()]
|
||||
{
|
||||
@for opt in group.items() {
|
||||
option
|
||||
value=(opt.value().as_str().unwrap_or(""))
|
||||
selected[*opt.selected()]
|
||||
disabled[*opt.disabled()]
|
||||
{
|
||||
(opt.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if *self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor de la lista de selección.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor seleccionado no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||
///
|
||||
/// Cuando está activo, la etiqueta se superpone al control y permanece flotante siempre que
|
||||
/// haya una opción visible.
|
||||
///
|
||||
/// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará los
|
||||
/// valores establecidos con [`with_multiple()`](Self::with_multiple) y
|
||||
/// [`with_rows()`](Self::with_rows) antes del renderizado.
|
||||
#[builder_fn]
|
||||
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||
self.floating_label = floating_label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un elemento individual a la lista de selección.
|
||||
///
|
||||
/// Los elementos y grupos se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.entries.push(Entry::Item(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un grupo de elementos a la lista de selección.
|
||||
///
|
||||
/// Los elementos y grupos se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_group(mut self, group: Group) -> Self {
|
||||
self.entries.push(Entry::Group(group));
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control permite seleccionar varios elementos.
|
||||
///
|
||||
/// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se
|
||||
/// recomienda combinar con [`with_rows()`](Self::with_rows) para controlar el número de filas
|
||||
/// visibles.
|
||||
///
|
||||
/// Para un número reducido de elementos con etiquetas descriptivas considera usar
|
||||
/// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en
|
||||
/// pantallas pequeñas.
|
||||
///
|
||||
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||
#[builder_fn]
|
||||
pub fn with_multiple(mut self, multiple: bool) -> Self {
|
||||
self.multiple = multiple;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el número de filas visibles de la lista de selección.
|
||||
///
|
||||
/// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de
|
||||
/// desplegable, tanto en modo simple como múltiple. Con `None` se omite el atributo y presenta
|
||||
/// el control como desplegable (comportamiento por defecto).
|
||||
///
|
||||
/// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin
|
||||
/// necesidad de recurrir al desplazamiento.
|
||||
///
|
||||
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||
#[builder_fn]
|
||||
pub fn with_rows(mut self, rows: Option<u16>) -> Self {
|
||||
self.rows.alter_opt(rows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la configuración de autocompletado del campo.
|
||||
///
|
||||
/// Permite al navegador rellenar automáticamente el elemento seleccionado en listas de países
|
||||
/// (`"country"`), idiomas (`"language"`), sexo (`"sex"`) u otros campos con valores
|
||||
/// predefinidos. En listas de selección múltiples no es útil en la práctica, ya que los
|
||||
/// navegadores no gestionan selecciones múltiples con autocompletado.
|
||||
///
|
||||
/// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para
|
||||
/// omitir el atributo.
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue