Compare commits
No commits in common. "main" and "legacy/upgrade-sea-orm-to-0_12_8" have entirely different histories.
main
...
legacy/upg
566 changed files with 17689 additions and 46944 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"
|
|
||||||
]
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,11 +1,6 @@
|
||||||
# Ignora directorios de compilación
|
|
||||||
**/target
|
**/target
|
||||||
|
|
||||||
# Archivos de log
|
|
||||||
**/log/*.log*
|
**/log/*.log*
|
||||||
|
|
||||||
# Archivos de configuración locales
|
|
||||||
**/local.*.toml
|
**/local.*.toml
|
||||||
**/local.toml
|
**/local.toml
|
||||||
.env
|
Cargo.lock
|
||||||
.vscode
|
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.
|
|
||||||
59
CREDITS.md
59
CREDITS.md
|
|
@ -1,40 +1,45 @@
|
||||||
# 🔃 Dependencias
|
# ⌨️ Código
|
||||||
|
|
||||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
* PageTop incluye código de la versión [0.11.0](https://github.com/mehcode/config-rs/tree/0.11.0) de
|
||||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs/) como son:
|
[config](https://crates.io/crates/config), de [Ryan Leckey](https://crates.io/users/mehcode), por
|
||||||
|
las facilidades que ofrece frente a sus versiones más modernas para leer los ajustes de
|
||||||
|
configuración y delegar su asignación a tipos seguros según los requerimientos de cada módulo,
|
||||||
|
tema o aplicación.
|
||||||
|
|
||||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
* PageTop incorpora una versión adaptada del excelente *crate* [maud](https://crates.io/crates/maud)
|
||||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
de [Chris Wong](https://crates.io/users/lambda-fairy) (versión
|
||||||
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
[0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud)), para añadir sus funcionalidades
|
||||||
de la aplicación.
|
sin requerir la referencia a `maud` en el archivo `Cargo.toml` de cada proyecto.
|
||||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
|
||||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
* PageTop usa los reconocidos *crates* [SQLx](https://github.com/launchbadge/sqlx) y
|
||||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
[SeaQuery](https://github.com/SeaQL/sea-query) para interactuar con bases de datos. Sin embargo,
|
||||||
PageTop y sus extensiones.
|
para restringir las migraciones a módulos, se ha integrado en el código una versión modificada de
|
||||||
|
[SeaORM Migration](https://github.com/SeaQL/sea-orm/tree/master/sea-orm-migration) (versión
|
||||||
|
[0.12.8](https://github.com/SeaQL/sea-orm/tree/0.12.8/sea-orm-migration/src)).
|
||||||
|
|
||||||
|
|
||||||
# 🗚 FIGfonts
|
# 🗚 FIGfonts
|
||||||
|
|
||||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
PageTop utiliza el paquete [figlet-rs](https://crates.io/crates/figlet-rs) de *yuanbohan* para
|
||||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
mostrar en el terminal un rótulo de presentación con el nombre de la aplicación usando caracteres
|
||||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `src/app` son:
|
||||||
|
|
||||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
|
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) por *Glenn Chappell*.
|
||||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) por *Glenn Chappell*
|
||||||
(predeterminada)
|
(predeterminado).
|
||||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) de *Claude Martins*
|
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) por *Claude Martins*.
|
||||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) por *Ryan Youck*.
|
||||||
|
|
||||||
|
|
||||||
# 🎨 CSS
|
# 📰 Plantillas
|
||||||
|
|
||||||
La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/)
|
* El diseño de la página predeterminada de inicio está basado en la plantilla
|
||||||
para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia
|
[Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc) creada
|
||||||
[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
por [inovatik](https://inovatik.com/) y distribuida por [ThemeWagon](https://themewagon.com).
|
||||||
|
|
||||||
|
|
||||||
# 👾 Icono
|
# 🎨 Icono
|
||||||
|
|
||||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
"La criatura" sonriente es una divertida 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
|
Puede encontrarse en su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty)
|
||||||
[ICONFINDER](https://www.iconfinder.com).
|
disponible en [ICONFINDER](https://www.iconfinder.com).
|
||||||
3276
Cargo.lock
generated
3276
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
106
Cargo.toml
106
Cargo.toml
|
|
@ -1,92 +1,22 @@
|
||||||
[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]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
# Helpers
|
"pagetop",
|
||||||
"helpers/pagetop-build",
|
# Utilities.
|
||||||
"helpers/pagetop-macros",
|
"pagetop-macros",
|
||||||
"helpers/pagetop-minimal",
|
"pagetop-build",
|
||||||
"helpers/pagetop-statics",
|
"pagetop-homedemo",
|
||||||
# Extensions
|
# Modules.
|
||||||
"extensions/pagetop-aliner",
|
"pagetop-admin",
|
||||||
"extensions/pagetop-bootsier",
|
"pagetop-user",
|
||||||
|
"pagetop-node",
|
||||||
|
# Themes.
|
||||||
|
"pagetop-bootsier",
|
||||||
|
"pagetop-bulmix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
exclude = [
|
||||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
"drust",
|
||||||
homepage = "https://pagetop.cillero.es"
|
"examples",
|
||||||
license = "MIT OR Apache-2.0"
|
"tests",
|
||||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
actix-web = { version = "4.13", default-features = false }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
# 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
|
|
||||||
pagetop = { version = "0.5", path = "." }
|
|
||||||
|
|
|
||||||
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.
|
|
||||||
209
README.md
209
README.md
|
|
@ -1,176 +1,89 @@
|
||||||
<div align="center">
|
<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/banner-pagetop.png" />
|
||||||
|
|
||||||
<h1>PageTop</h1>
|
<h1>PageTop</h1>
|
||||||
|
|
||||||
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
|
[](https://crates.io/crates/pagetop)
|
||||||
|
[](https://docs.rs/pagetop)
|
||||||
|
|
||||||
[](https://docs.rs/pagetop)
|
|
||||||
[](https://crates.io/crates/pagetop)
|
|
||||||
[](https://crates.io/crates/pagetop)
|
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
**PageTop** es un entorno de desarrollo basado en [Rust](https://www.rust-lang.org/es/) que reúne
|
||||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
algunos de los crates más estables y populares para crear soluciones web modulares, extensibles y
|
||||||
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
configurables.
|
||||||
según las necesidades de cada proyecto, incluyendo:
|
|
||||||
|
|
||||||
* **Acciones** (*actions*): alteran la lógica interna de una funcionalidad interceptando su flujo
|
Incluye **Drust**, un sistema de gestión de contenidos basado en PageTop que permite crear, editar y
|
||||||
de ejecución.
|
mantener sitios web dinámicos, rápidos y seguros.
|
||||||
* **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.
|
|
||||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
|
||||||
componentes.
|
|
||||||
|
|
||||||
|
|
||||||
## ⚡️ Guía rápida
|
# 🚧 Advertencia
|
||||||
|
|
||||||
La aplicación más sencilla de PageTop se ve así:
|
**PageTop** es un proyecto personal para aprender Rust y conocer su ecosistema. Sólo se liberan
|
||||||
|
versiones de desarrollo. En este contexto la API no es estable y los cambios son constantes. No
|
||||||
```rust,no_run
|
puede considerarse preparado hasta que se libere la versión **0.1.0**.
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::new().run()?.await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
|
|
||||||
|
|
||||||
```rust,no_run
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
struct HelloWorld;
|
|
||||||
|
|
||||||
impl Extension for HelloWorld {
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
|
||||||
scfg.route("/", service::web::get().to(hello_world));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|
||||||
Page::new(request)
|
|
||||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
|
||||||
.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&HelloWorld).run()?.await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
|
||||||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
|
||||||
|
|
||||||
|
|
||||||
## 📂 Proyecto
|
# 📂 Estructura del código
|
||||||
|
|
||||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
El repositorio se organiza en un *workspace* con los siguientes subproyectos:
|
||||||
|
|
||||||
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
|
* **[pagetop](https://github.com/manuelcillero/pagetop/tree/main/pagetop)**, es la librería esencial
|
||||||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
construida con *crates* estables y muy conocidos del ecosistema Rust para proporcionar APIs,
|
||||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
patrones de desarrollo y buenas prácticas para la creación avanzada de soluciones web SSR
|
||||||
|
(*Server-Side Rendering*).
|
||||||
|
|
||||||
### Auxiliares
|
## Auxiliares
|
||||||
|
|
||||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
* **[pagetop-macros](https://github.com/manuelcillero/pagetop/tree/main/pagetop-macros)**, agrupa
|
||||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
las principales macros procedurales para usar desde **PageTop**.
|
||||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
|
||||||
|
|
||||||
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
* **[pagetop-build](https://github.com/manuelcillero/pagetop/tree/main/pagetop-build)**, permite
|
||||||
proporciona una colección de macros procedurales que mejoran la experiencia de desarrollo con
|
incluir fácilmente recursos en los archivos binarios al compilar aplicaciones creadas con
|
||||||
PageTop.
|
**PageTop**.
|
||||||
|
|
||||||
* **[pagetop-minimal](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal)**,
|
## Módulos
|
||||||
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.
|
|
||||||
|
|
||||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
* **[pagetop-homedemo](https://github.com/manuelcillero/pagetop/tree/main/pagetop-homedemo)**,
|
||||||
permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos
|
módulo que muestra una página de inicio de demostración para presentar **PageTop**.
|
||||||
de forma eficiente, con detección de cambios que optimizan el tiempo de compilación.
|
|
||||||
|
|
||||||
### Extensiones
|
* **[pagetop-admin](https://github.com/manuelcillero/pagetop/tree/main/pagetop-admin)**, módulo que
|
||||||
|
proporciona a otros módulos un lugar común donde presentar a los administradores sus opciones de
|
||||||
|
configuración.
|
||||||
|
|
||||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
* **[pagetop-user](https://github.com/manuelcillero/pagetop/tree/main/pagetop-user)**, módulo para
|
||||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
añadir gestión de usuarios, roles, permisos y sesiones en aplicaciones desarrolladas con PageTop.
|
||||||
|
|
||||||
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
|
* **[pagetop-node](https://github.com/manuelcillero/pagetop/tree/main/pagetop-node)**, módulo para
|
||||||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
crear, extender o personalizar los tipos de contenido que puede administrar un sitio web.
|
||||||
componentes flexibles.
|
|
||||||
|
## Temas
|
||||||
|
|
||||||
|
* **[pagetop-bootsier](https://github.com/manuelcillero/pagetop/tree/main/pagetop-bootsier)**, tema
|
||||||
|
que utiliza el *framework* [Bootstrap](https://getbootstrap.com/) para la composición de páginas y
|
||||||
|
visualización de componentes.
|
||||||
|
|
||||||
|
* **[pagetop-bulmix](https://github.com/manuelcillero/pagetop/tree/main/pagetop-bulmix)**, tema que
|
||||||
|
utiliza el *framework* [Bulma](https://bulma.io/) para la composición de páginas y visualización
|
||||||
|
de componentes.
|
||||||
|
|
||||||
|
## Aplicación
|
||||||
|
|
||||||
|
* **[drust](https://github.com/manuelcillero/pagetop/tree/main/drust)**, es una aplicación
|
||||||
|
inspirada modestamente en [Drupal](https://www.drupal.org) que utiliza PageTop para crear un CMS
|
||||||
|
(*Content Management System* o sistema de gestión de contenidos) para construir sitios web
|
||||||
|
dinámicos, administrados y configurables.
|
||||||
|
|
||||||
|
|
||||||
## 🧪 Pruebas
|
# 📜 Licencia
|
||||||
|
|
||||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
Este proyecto tiene licencia, de hecho tiene dos, puedes aplicar cualquiera de las siguientes a tu
|
||||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
elección:
|
||||||
|
|
||||||
| Comando | Descripción |
|
* Licencia Apache versión 2.0
|
||||||
| ------- | ----------- |
|
([LICENSE-APACHE](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-APACHE) o
|
||||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
[http://www.apache.org/licenses/LICENSE-2.0]).
|
||||||
| `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**
|
* Licencia MIT
|
||||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
([LICENSE-MIT](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-MIT) o
|
||||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
[http://opensource.org/licenses/MIT]).
|
||||||
|
|
||||||
|
|
||||||
## 🚧 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.
|
|
||||||
|
|
||||||
|
|
||||||
## ✨ 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).
|
|
||||||
|
|
||||||
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
|
|
||||||
licencia *Apache v2.0*.
|
|
||||||
|
|
|
||||||
BIN
banner-pagetop.png
Normal file
BIN
banner-pagetop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
|
|
@ -1,2 +0,0 @@
|
||||||
[log]
|
|
||||||
tracing = "Info,pagetop=Debug"
|
|
||||||
25
drust/Cargo.toml
Normal file
25
drust/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "drust"
|
||||||
|
version = "0.0.3"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
authors = [
|
||||||
|
"Manuel Cillero <manuel@cillero.es>"
|
||||||
|
]
|
||||||
|
description = """\
|
||||||
|
A modern web Content Management System to share your world.\
|
||||||
|
"""
|
||||||
|
homepage = "https://pagetop.cillero.es"
|
||||||
|
repository = "https://github.com/manuelcillero/pagetop"
|
||||||
|
license = "Apache-2.0 OR MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pagetop = { version = "0.0", path = "../pagetop", features = ["mysql"], default-features = false }
|
||||||
|
# Themes.
|
||||||
|
pagetop-bootsier = { version = "0.0", path = "../pagetop-bootsier" }
|
||||||
|
pagetop-bulmix = { version = "0.0", path = "../pagetop-bulmix" }
|
||||||
|
# Modules.
|
||||||
|
pagetop-homedemo = { version = "0.0", path = "../pagetop-homedemo" }
|
||||||
|
pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
|
||||||
|
pagetop-user = { version = "0.0", path = "../pagetop-user" }
|
||||||
|
pagetop-node = { version = "0.0", path = "../pagetop-node" }
|
||||||
28
drust/README.md
Normal file
28
drust/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
**Drust** es una aplicación inspirada modestamente en [Drupal](https://www.drupal.org) que utiliza
|
||||||
|
**PageTop** para crear un CMS (*Content Management System* o sistema de gestión de contenidos) para
|
||||||
|
construir sitios web dinámicos, administrados y configurables.
|
||||||
|
|
||||||
|
[PageTop](https://github.com/manuelcillero/pagetop/tree/main/pagetop), es un entorno de desarrollo
|
||||||
|
basado en algunos de los *crates* más estables y populares del ecosistema Rust para proporcionar
|
||||||
|
APIs, patrones de desarrollo y buenas prácticas para la creación de soluciones web SSR (*Server-Side
|
||||||
|
Rendering*).
|
||||||
|
|
||||||
|
|
||||||
|
# 🚧 Advertencia
|
||||||
|
|
||||||
|
**PageTop** sólo libera actualmente versiones de desarrollo. La API no es estable y los cambios son
|
||||||
|
constantes. No puede considerarse preparado hasta que se libere la versión **0.1.0**.
|
||||||
|
|
||||||
|
|
||||||
|
# 📜 Licencia
|
||||||
|
|
||||||
|
Este proyecto tiene licencia, de hecho tiene dos, puedes aplicar cualquiera de las siguientes a tu
|
||||||
|
elección:
|
||||||
|
|
||||||
|
* Licencia Apache versión 2.0
|
||||||
|
([LICENSE-APACHE](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-APACHE) o
|
||||||
|
[http://www.apache.org/licenses/LICENSE-2.0]).
|
||||||
|
|
||||||
|
* Licencia MIT
|
||||||
|
([LICENSE-MIT](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-MIT) o
|
||||||
|
[http://opensource.org/licenses/MIT]).
|
||||||
8
drust/config/common.toml
Normal file
8
drust/config/common.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[app]
|
||||||
|
name = "Drust"
|
||||||
|
description = """\
|
||||||
|
A modern web Content Management System to share your world.\
|
||||||
|
"""
|
||||||
|
|
||||||
|
[database]
|
||||||
|
db_type = "mysql"
|
||||||
10
drust/config/default.toml
Normal file
10
drust/config/default.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[app]
|
||||||
|
#theme = "Basic"
|
||||||
|
#theme = "Chassis"
|
||||||
|
theme = "Inception"
|
||||||
|
#theme = "Bootsier"
|
||||||
|
#theme = "Bulmix"
|
||||||
|
language = "es-ES"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||||
30
drust/src/main.rs
Normal file
30
drust/src/main.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[derive(BindHandle)]
|
||||||
|
struct Drust;
|
||||||
|
|
||||||
|
impl ModuleTrait for Drust {
|
||||||
|
fn dependencies(&self) -> Vec<ModuleRef> {
|
||||||
|
vec![
|
||||||
|
// Themes.
|
||||||
|
&pagetop_bootsier::Bootsier,
|
||||||
|
&pagetop_bulmix::Bulmix,
|
||||||
|
// Modules.
|
||||||
|
&pagetop_homedemo::HomeDemo,
|
||||||
|
&pagetop_admin::Admin,
|
||||||
|
&pagetop_user::User,
|
||||||
|
&pagetop_node::Node,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_modules(&self) -> Vec<ModuleRef> {
|
||||||
|
vec![
|
||||||
|
// &pagetop_node::Node
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&Drust).unwrap().run()?.await
|
||||||
|
}
|
||||||
4
examples/Cargo.toml
Normal file
4
examples/Cargo.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"basics/*",
|
||||||
|
]
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::new().run()?.await
|
|
||||||
}
|
|
||||||
9
examples/basics/hello-name/Cargo.toml
Normal file
9
examples/basics/hello-name/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "hello_name"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "4"
|
||||||
|
pagetop = { version = "0.0", path = "../../../pagetop" }
|
||||||
|
|
@ -1,28 +1,26 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[derive(BindHandle)]
|
||||||
struct HelloName;
|
struct HelloName;
|
||||||
|
|
||||||
impl Extension for HelloName {
|
impl ModuleTrait for HelloName {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||||
scfg.route("/hello/{name}", service::web::get().to(hello_name));
|
scfg.service(hello_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[service::get("/hello/{name}")]
|
||||||
async fn hello_name(
|
async fn hello_name(
|
||||||
request: HttpRequest,
|
request: service::HttpRequest,
|
||||||
path: service::web::Path<String>,
|
path: service::web::Path<String>,
|
||||||
) -> ResultPage<Markup, ErrorPage> {
|
) -> ResultPage<Markup, ErrorPage> {
|
||||||
let name = path.into_inner();
|
let name = path.into_inner();
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(Html::with(move |_| {
|
.with_component_in("content", Html::with(html! { h1 { "Hello " (name) "!" } }))
|
||||||
html! {
|
|
||||||
h1 style="text-align: center;" { "Hello " (name) "!" }
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&HelloName).run()?.await
|
Application::prepare(&HelloName).unwrap().run()?.await
|
||||||
}
|
}
|
||||||
8
examples/basics/hello-world/Cargo.toml
Normal file
8
examples/basics/hello-world/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "hello_world"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pagetop = { version = "0.0", path = "../../../pagetop" }
|
||||||
21
examples/basics/hello-world/src/main.rs
Normal file
21
examples/basics/hello-world/src/main.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[derive(BindHandle)]
|
||||||
|
struct HelloWorld;
|
||||||
|
|
||||||
|
impl ModuleTrait for HelloWorld {
|
||||||
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||||
|
scfg.route("/", service::web::get().to(hello_world));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn hello_world(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_component_in("content", Html::with(html! { h1 { "Hello World!" } }))
|
||||||
|
.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&HelloWorld).unwrap().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,24 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
struct HelloWorld;
|
|
||||||
|
|
||||||
impl Extension for HelloWorld {
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
|
||||||
scfg.route("/", service::web::get().to(hello_world));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|
||||||
Page::new(request)
|
|
||||||
.with_child(Html::with(|_| {
|
|
||||||
html! {
|
|
||||||
h1 style="text-align: center;" { "Hello World!" }
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&HelloWorld).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,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 @@
|
||||||
[package]
|
|
||||||
name = "pagetop-aliner"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
|
||||||
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML
|
|
||||||
"""
|
|
||||||
categories = ["web-programming", "gui"]
|
|
||||||
keywords = ["pagetop", "theme", "css"]
|
|
||||||
|
|
||||||
repository.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
pagetop.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.
|
|
||||||
|
|
@ -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
extensions/pagetop-bootsier/.gitattributes
vendored
1
extensions/pagetop-bootsier/.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
static/** linguist-vendored
|
|
||||||
|
|
@ -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 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "pagetop-bootsier"
|
|
||||||
version = "0.1.1"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
|
||||||
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
|
||||||
"""
|
|
||||||
categories = ["web-programming", "gui"]
|
|
||||||
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
|
||||||
|
|
||||||
repository.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
pagetop.workspace = true
|
|
||||||
serde.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.
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<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;
|
|
||||||
|
|
||||||
impl Extension for MyApp {
|
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
|
||||||
vec![
|
|
||||||
// ...
|
|
||||||
&pagetop_bootsier::Bootsier,
|
|
||||||
// ...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&MyApp).run()?.await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Y **selecciona el tema en la configuración** de la aplicación:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[app]
|
|
||||||
theme = "Bootsier"
|
|
||||||
```
|
|
||||||
|
|
||||||
o **fuerza el tema por código** en una página concreta:
|
|
||||||
|
|
||||||
```rust,no_run
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
use pagetop_bootsier::Bootsier;
|
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|
||||||
Page::new(request)
|
|
||||||
.with_theme(&Bootsier)
|
|
||||||
.add_child(
|
|
||||||
Block::new()
|
|
||||||
.with_title(L10n::l("sample_title"))
|
|
||||||
.add_child(Html::with(|cx| html! {
|
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.render()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 🚧 Advertencia
|
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
|
||||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
|
||||||
hasta que se libere la versión **1.0.0**.
|
|
||||||
|
|
||||||
|
|
||||||
## 📜 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,20 +0,0 @@
|
||||||
use pagetop_build::StaticFilesBundle;
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
|
||||||
StaticFilesBundle::from_scss("./static/scss/bootsier.scss", "bootstrap.min.css")
|
|
||||||
.with_name("bootsier_bs")
|
|
||||||
.build()?;
|
|
||||||
StaticFilesBundle::from_dir("./static/js", Some(bootstrap_js_files))
|
|
||||||
.with_name("bootsier_js")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
//! Opciones de configuración del tema.
|
|
||||||
//!
|
|
||||||
//! Ejemplo:
|
|
||||||
//!
|
|
||||||
//! ```toml
|
|
||||||
//! [bootsier]
|
|
||||||
//! max_width = "90rem"
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Uso:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! use pagetop_bootsier::config;
|
|
||||||
//!
|
|
||||||
//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
|
|
||||||
//! los valores a los ajustes.
|
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
include_config!(SETTINGS: Settings => [
|
|
||||||
// [bootsier]
|
|
||||||
"bootsier.max_width" => "1440px",
|
|
||||||
]);
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
|
|
||||||
pub struct Settings {
|
|
||||||
pub bootsier: Bootsier,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
|
|
||||||
pub struct Bootsier {
|
|
||||||
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
|
||||||
pub max_width: UnitValue,
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
/*!
|
|
||||||
<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;
|
|
||||||
|
|
||||||
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_locales!(LOCALES_BOOTSIER);
|
|
||||||
|
|
||||||
// Versión de la librería Bootstrap.
|
|
||||||
const BOOTSTRAP_VERSION: &str = "5.3.8";
|
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
|
|
||||||
pub mod theme;
|
|
||||||
|
|
||||||
/// *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 {
|
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
|
||||||
Some(&Self)
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme for Bootsier {
|
|
||||||
#[inline]
|
|
||||||
fn default_template(&self) -> TemplateRef {
|
|
||||||
&BootsierTemplate::Standard
|
|
||||||
}
|
|
||||||
|
|
||||||
fn before_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),
|
|
||||||
))
|
|
||||||
.alter_assets(AssetsOp::AddJavaScript(
|
|
||||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
|
||||||
.with_weight(-90),
|
|
||||||
))
|
|
||||||
.alter_child_in(
|
|
||||||
&DefaultRegion::Footer,
|
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
# Dropdown
|
|
||||||
dropdown_toggle = Toggle Dropdown
|
|
||||||
|
|
||||||
# form::Input
|
|
||||||
input_required = This field is required
|
|
||||||
|
|
||||||
# Navbar
|
|
||||||
toggle = Toggle navigation
|
|
||||||
|
|
||||||
# Offcanvas
|
|
||||||
offcanvas_close = Close
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
# Dropdown
|
|
||||||
dropdown_toggle = Mostrar/ocultar menú
|
|
||||||
|
|
||||||
# form::Input
|
|
||||||
input_required = Este campo es obligatorio
|
|
||||||
|
|
||||||
# Navbar
|
|
||||||
toggle = Mostrar/ocultar navegación
|
|
||||||
|
|
||||||
# Offcanvas
|
|
||||||
offcanvas_close = Cerrar
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::form;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
/// Componente para crear un **área de texto** de formulario.
|
|
||||||
///
|
|
||||||
/// Permite escribir en un área de texto de más de una línea, con una etiqueta opcional y atributos
|
|
||||||
/// como el número de filas a presentar, longitud mínima (`minlength`) y máxima (`maxlength`), texto
|
|
||||||
/// indicativo (`placeholder`) o autocompletado (`autocomplete`).
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop::prelude::*;
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let descripcion = form::Textarea::new()
|
|
||||||
/// .with_name("description")
|
|
||||||
/// .with_label(L10n::n("Description"))
|
|
||||||
/// .with_rows(Some(8))
|
|
||||||
/// .with_maxlength(Some(500))
|
|
||||||
/// .with_placeholder(L10n::n("Write here..."))
|
|
||||||
/// .with_required(true);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un área de texto siempre envía su
|
|
||||||
/// valor, incluso si está vacía. En el servidor se deserializa como `String`:
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// #[derive(serde::Deserialize)]
|
|
||||||
/// struct FormData {
|
|
||||||
/// description: String, // Siempre presente; cadena vacía si el usuario no escribió nada.
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Textarea {
|
|
||||||
#[getters(skip)]
|
|
||||||
id: AttrId,
|
|
||||||
/// Devuelve las clases CSS del contenedor del área de texto.
|
|
||||||
classes: Classes,
|
|
||||||
/// Devuelve el nombre del campo.
|
|
||||||
name: AttrName,
|
|
||||||
/// Devuelve el valor inicial del área de texto.
|
|
||||||
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 el número de filas visibles del área de texto.
|
|
||||||
rows: Attr<u16>,
|
|
||||||
/// 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 área de texto.
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Textarea {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
if *self.floating_label() {
|
|
||||||
self.rows.alter_opt(None::<u16>);
|
|
||||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
|
||||||
}
|
|
||||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-textarea");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 textarea_id = container_id
|
|
||||||
.as_deref()
|
|
||||||
.map(|id| util::join!(id, "-textarea"));
|
|
||||||
// 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=[textarea_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)
|
|
||||||
}
|
|
||||||
textarea
|
|
||||||
id=[textarea_id.as_deref()]
|
|
||||||
class="form-control"
|
|
||||||
name=[self.name().get()]
|
|
||||||
rows=[self.rows().get()]
|
|
||||||
minlength=[self.minlength().get()]
|
|
||||||
maxlength=[self.maxlength().get()]
|
|
||||||
placeholder=[placeholder]
|
|
||||||
autocomplete=[self.autocomplete().get()]
|
|
||||||
autofocus[*self.autofocus()]
|
|
||||||
readonly[*self.readonly()]
|
|
||||||
required[*self.required()]
|
|
||||||
disabled[*self.disabled()]
|
|
||||||
{
|
|
||||||
@if let Some(value) = self.value().get() {
|
|
||||||
(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if *self.floating_label() {
|
|
||||||
(label)
|
|
||||||
}
|
|
||||||
@if let Some(description) = self.help_text().lookup(cx) {
|
|
||||||
div class="form-text" { (description) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Textarea {
|
|
||||||
// **< Textarea 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 área de texto.
|
|
||||||
#[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 área de texto y asciende al enfocarlo o
|
|
||||||
/// cuando tiene contenido.
|
|
||||||
///
|
|
||||||
/// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará el valor
|
|
||||||
/// establecido con [`with_rows()`](Self::with_rows) antes del renderizado. Si es necesario, se
|
|
||||||
/// puede controlar la altura con estilos aplicados al componente.
|
|
||||||
#[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 el número de filas visibles del área de texto.
|
|
||||||
///
|
|
||||||
/// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el
|
|
||||||
/// estándar.
|
|
||||||
///
|
|
||||||
/// 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 longitud mínima permitida en caracteres.
|
|
||||||
#[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.
|
|
||||||
#[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 área de texto (`None` para quitarlo).
|
|
||||||
///
|
|
||||||
/// Este texto aparece en el área de texto 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.
|
|
||||||
///
|
|
||||||
/// Permite al navegador sugerir o rellenar automáticamente el contenido del área de texto
|
|
||||||
/// con valores guardados. Es especialmente útil en áreas con contenido semántico predefinido.
|
|
||||||
///
|
|
||||||
/// 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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
|
||||||
|
|
||||||
#[derive(AutoDefault, Clone)]
|
|
||||||
pub enum IconKind {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Font(FontSize),
|
|
||||||
Svg {
|
|
||||||
shapes: Markup,
|
|
||||||
viewbox: AttrValue,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Icon {
|
|
||||||
/// Devuelve las clases CSS asociadas al icono.
|
|
||||||
classes: Classes,
|
|
||||||
icon_kind: IconKind,
|
|
||||||
aria_label: AttrL10n,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Icon {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
if !matches!(self.icon_kind(), IconKind::None) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
|
||||||
}
|
|
||||||
if let IconKind::Font(font_size) = self.icon_kind() {
|
|
||||||
self.alter_classes(ClassesOp::Add, font_size.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
Ok(match self.icon_kind() {
|
|
||||||
IconKind::None => html! {},
|
|
||||||
IconKind::Font(_) => {
|
|
||||||
let aria_label = self.aria_label().lookup(cx);
|
|
||||||
let has_label = aria_label.is_some();
|
|
||||||
html! {
|
|
||||||
i
|
|
||||||
class=[self.classes().get()]
|
|
||||||
role=[has_label.then_some("img")]
|
|
||||||
aria-label=[aria_label]
|
|
||||||
aria-hidden=[(!has_label).then_some("true")]
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconKind::Svg { shapes, viewbox } => {
|
|
||||||
let aria_label = self.aria_label().lookup(cx);
|
|
||||||
let has_label = aria_label.is_some();
|
|
||||||
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
|
||||||
html! {
|
|
||||||
svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox=(viewbox)
|
|
||||||
fill="currentColor"
|
|
||||||
focusable="false"
|
|
||||||
class=[self.classes().get()]
|
|
||||||
role=[has_label.then_some("img")]
|
|
||||||
aria-label=[aria_label]
|
|
||||||
aria-hidden=[(!has_label).then_some("true")]
|
|
||||||
{
|
|
||||||
(shapes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Icon {
|
|
||||||
pub fn font() -> Self {
|
|
||||||
Self::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_sized(font_size: FontSize) -> Self {
|
|
||||||
Self::default().with_icon_kind(IconKind::Font(font_size))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn svg(shapes: Markup) -> Self {
|
|
||||||
Self::default().with_icon_kind(IconKind::Svg {
|
|
||||||
shapes,
|
|
||||||
viewbox: AttrValue::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
|
||||||
Self::default().with_icon_kind(IconKind::Svg {
|
|
||||||
shapes,
|
|
||||||
viewbox: AttrValue::new(viewbox),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Icon BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al icono.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self {
|
|
||||||
self.icon_kind = icon_kind;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_aria_label(mut self, label: L10n) -> Self {
|
|
||||||
self.aria_label.alter_value(label);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//! Definiciones para renderizar imágenes ([`Image`]).
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Size, Source};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Image;
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para renderizar una **imagen**.
|
|
||||||
///
|
|
||||||
/// - Ajusta su disposición según el origen definido en [`image::Source`].
|
|
||||||
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
|
||||||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
|
||||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
|
||||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Image {
|
|
||||||
#[getters(skip)]
|
|
||||||
id: AttrId,
|
|
||||||
/// Devuelve las clases CSS asociadas a la imagen.
|
|
||||||
classes: Classes,
|
|
||||||
/// Devuelve las dimensiones de la imagen.
|
|
||||||
size: image::Size,
|
|
||||||
/// Devuelve el origen de la imagen.
|
|
||||||
source: image::Source,
|
|
||||||
/// Devuelve el texto alternativo localizado.
|
|
||||||
alternative: Attr<L10n>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Image {
|
|
||||||
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.source().to_class());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
let dimensions = self.size().to_style();
|
|
||||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
|
||||||
let is_decorative = alt_text.is_empty();
|
|
||||||
let source = match self.source() {
|
|
||||||
image::Source::Logo(logo) => {
|
|
||||||
return Ok(html! {
|
|
||||||
span
|
|
||||||
id=[self.id()]
|
|
||||||
class=[self.classes().get()]
|
|
||||||
style=[dimensions]
|
|
||||||
role=[(!is_decorative).then_some("img")]
|
|
||||||
aria-label=[(!is_decorative).then_some(alt_text)]
|
|
||||||
aria-hidden=[is_decorative.then_some("true")]
|
|
||||||
{
|
|
||||||
(logo.render(cx))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
image::Source::Responsive(source) => Some(source),
|
|
||||||
image::Source::Thumbnail(source) => Some(source),
|
|
||||||
image::Source::Plain(source) => Some(source),
|
|
||||||
};
|
|
||||||
Ok(html! {
|
|
||||||
img
|
|
||||||
src=[source]
|
|
||||||
alt=(alt_text)
|
|
||||||
id=[self.id()]
|
|
||||||
class=[self.classes().get()]
|
|
||||||
style=[dimensions] {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Image {
|
|
||||||
/// Crea rápidamente una imagen especificando su origen.
|
|
||||||
pub fn with(source: image::Source) -> Self {
|
|
||||||
Self::default().with_source(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Image BUILDER >**************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la imagen.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas a la imagen.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Establecer bordes ([`classes::Border`]).
|
|
||||||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_classes(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_size(mut self, size: image::Size) -> Self {
|
|
||||||
self.size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_source(mut self, source: image::Source) -> Self {
|
|
||||||
self.source = source;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define un *texto localizado* ([`L10n`]) alternativo para la imagen.
|
|
||||||
///
|
|
||||||
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
|
|
||||||
/// decorativa.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_alternative(mut self, alt: L10n) -> Self {
|
|
||||||
self.alternative.alter_value(alt);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Size >***************************************************************************************
|
|
||||||
|
|
||||||
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::Image)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Size {
|
|
||||||
/// Ajuste automático por defecto.
|
|
||||||
///
|
|
||||||
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
/// Establece explícitamente el **ancho y alto** de la imagen.
|
|
||||||
///
|
|
||||||
/// Útil cuando se desea fijar ambas dimensiones de forma exacta. Ten en cuenta que la imagen
|
|
||||||
/// puede distorsionarse si no se mantiene la proporción original.
|
|
||||||
Dimensions(UnitValue, UnitValue),
|
|
||||||
/// Establece sólo el **ancho** de la imagen.
|
|
||||||
///
|
|
||||||
/// La altura se ajusta proporcionalmente de manera automática.
|
|
||||||
Width(UnitValue),
|
|
||||||
/// Establece sólo la **altura** de la imagen.
|
|
||||||
///
|
|
||||||
/// El ancho se ajusta proporcionalmente de manera automática.
|
|
||||||
Height(UnitValue),
|
|
||||||
/// Establece **el mismo valor** para el ancho y el alto de la imagen.
|
|
||||||
///
|
|
||||||
/// Práctico para forzar rápidamente un área cuadrada. Ten en cuenta que la imagen puede
|
|
||||||
/// distorsionarse si la original no es cuadrada.
|
|
||||||
Both(UnitValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Size {
|
|
||||||
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_style(self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Auto => None,
|
|
||||||
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
|
|
||||||
Self::Width(w) => Some(format!("width: {w};")),
|
|
||||||
Self::Height(h) => Some(format!("height: {h};")),
|
|
||||||
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Source >*************************************************************************************
|
|
||||||
|
|
||||||
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::Image)).
|
|
||||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
|
||||||
pub enum Source {
|
|
||||||
/// Imagen con el logotipo de PageTop.
|
|
||||||
#[default]
|
|
||||||
Logo(PageTopSvg),
|
|
||||||
/// Imagen que se adapta automáticamente a su contenedor.
|
|
||||||
///
|
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
|
||||||
Responsive(CowStr),
|
|
||||||
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
|
||||||
///
|
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
|
||||||
Thumbnail(CowStr),
|
|
||||||
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
|
|
||||||
///
|
|
||||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
|
||||||
Plain(CowStr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Source {
|
|
||||||
const IMG_FLUID: &str = "img-fluid";
|
|
||||||
const IMG_THUMBNAIL: &str = "img-thumbnail";
|
|
||||||
|
|
||||||
/// Imagen con el logotipo de PageTop.
|
|
||||||
#[inline]
|
|
||||||
pub fn logo(svg: PageTopSvg) -> Self {
|
|
||||||
Self::Logo(svg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Imagen responsive (`img-fluid`).
|
|
||||||
#[inline]
|
|
||||||
pub fn responsive(url: impl Into<CowStr>) -> Self {
|
|
||||||
Self::Responsive(url.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Imagen miniatura (`img-thumbnail`).
|
|
||||||
#[inline]
|
|
||||||
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
|
|
||||||
Self::Thumbnail(url.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Imagen sin clases adicionales.
|
|
||||||
#[inline]
|
|
||||||
pub fn plain(url: impl Into<CowStr>) -> Self {
|
|
||||||
Self::Plain(url.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase base asociada a la imagen según la fuente.
|
|
||||||
#[inline]
|
|
||||||
fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
|
|
||||||
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
|
|
||||||
Source::Plain(_) => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade la clase base asociada a la imagen según la fuente a la cadena de clases (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
|
||||||
let s = self.as_str();
|
|
||||||
if s.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(s);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/// Devuelve la clase asociada a la imagen según la fuente.
|
|
||||||
pub fn to_class(&self) -> String {
|
|
||||||
let s = self.as_str();
|
|
||||||
if s.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
let mut class = String::with_capacity(s.len());
|
|
||||||
class.push_str(s);
|
|
||||||
class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
|
|
||||||
//!
|
|
||||||
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
|
|
||||||
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
|
|
||||||
//! desplegables [`Dropdown`](crate::theme::Dropdown).
|
|
||||||
//!
|
|
||||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
|
||||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
|
||||||
//!
|
|
||||||
//! # Ejemplo
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let nav = Nav::tabs()
|
|
||||||
//! .with_layout(nav::Layout::End)
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
|
||||||
//! .with_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Options"))
|
|
||||||
//! .with_item(ChildOp::AddMany(vec![
|
|
||||||
//! dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
|
|
||||||
//! dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
|
|
||||||
//! ])),
|
|
||||||
//! ))
|
|
||||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Kind, Layout};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Nav;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::{Item, ItemKind};
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
|
|
||||||
///
|
|
||||||
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
|
||||||
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
|
|
||||||
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
|
||||||
///
|
|
||||||
/// Ver ejemplo en el módulo [`nav`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Nav {
|
|
||||||
#[getters(skip)]
|
|
||||||
id: AttrId,
|
|
||||||
/// Devuelve las clases CSS asociadas al menú.
|
|
||||||
classes: Classes,
|
|
||||||
/// Devuelve el estilo visual seleccionado.
|
|
||||||
nav_kind: nav::Kind,
|
|
||||||
/// Devuelve la distribución y orientación seleccionada.
|
|
||||||
nav_layout: nav::Layout,
|
|
||||||
/// Devuelve la lista de elementos del menú.
|
|
||||||
items: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Nav {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
|
||||||
let mut classes = "nav".to_string();
|
|
||||||
self.nav_kind().push_class(&mut classes);
|
|
||||||
self.nav_layout().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
ul id=[self.id()] class=[self.classes().get()] {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Nav {
|
|
||||||
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
|
||||||
pub fn tabs() -> Self {
|
|
||||||
Self::default().with_kind(nav::Kind::Tabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un `Nav` usando botones para los elementos (*Pills*).
|
|
||||||
pub fn pills() -> Self {
|
|
||||||
Self::default().with_kind(nav::Kind::Pills)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
|
||||||
pub fn underline() -> Self {
|
|
||||||
Self::default().with_kind(nav::Kind::Underline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Nav BUILDER >****************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_classes(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
|
|
||||||
self.nav_kind = kind;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selecciona la distribución y orientación del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
|
||||||
self.nav_layout = layout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
|
||||||
/// [`ChildOp`].
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// nav.with_item(nav::Item::link("Inicio", "/"));
|
|
||||||
/// nav.with_item(ChildOp::AddMany(vec![
|
|
||||||
/// nav::Item::link(...).into(),
|
|
||||||
/// nav::Item::link_disabled(...).into(),
|
|
||||||
/// ]));
|
|
||||||
/// ```
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
|
||||||
self.items.alter_child(op.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,299 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
// **< ItemKind >***********************************************************************************
|
|
||||||
|
|
||||||
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
|
|
||||||
/// [`Nav`](crate::theme::Nav).
|
|
||||||
///
|
|
||||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
|
||||||
/// con él.
|
|
||||||
#[derive(AutoDefault, 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,
|
|
||||||
},
|
|
||||||
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
|
||||||
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
|
||||||
Html(Embed<Html>),
|
|
||||||
/// Elemento que despliega un menú [`Dropdown`].
|
|
||||||
Dropdown(Embed<Dropdown>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemKind {
|
|
||||||
const ITEM: &str = "nav-item";
|
|
||||||
const DROPDOWN: &str = "nav-item dropdown";
|
|
||||||
|
|
||||||
// Devuelve las clases base asociadas al tipo de elemento.
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Void => "",
|
|
||||||
Self::Dropdown(_) => Self::DROPDOWN,
|
|
||||||
_ => Self::ITEM,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/// Devuelve las clases asociadas al tipo de elemento.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(&self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item >***************************************************************************************
|
|
||||||
|
|
||||||
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
|
|
||||||
///
|
|
||||||
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
|
|
||||||
/// puede comportarse como texto, enlace, contenido HTML o menú desplegable, 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 setup(&mut self, _cx: &Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
|
||||||
}
|
|
||||||
|
|
||||||
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="nav-link disabled" aria-disabled="true" {
|
|
||||||
(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 = "nav-link".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");
|
|
||||||
|
|
||||||
html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
a
|
|
||||||
class=(classes)
|
|
||||||
href=[href]
|
|
||||||
target=[target]
|
|
||||||
rel=[rel]
|
|
||||||
aria-current=[aria_current]
|
|
||||||
aria-disabled=[aria_disabled]
|
|
||||||
{
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemKind::Html(html) => html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
(html.render(cx))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ItemKind::Dropdown(menu) => {
|
|
||||||
if let Some(dd) = menu.get() {
|
|
||||||
let items = dd.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
|
||||||
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
|
||||||
.lookup(cx)
|
|
||||||
.unwrap_or_else(|| "Dropdown".to_string())
|
|
||||||
});
|
|
||||||
html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
a
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
href="#"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
{
|
|
||||||
(title)
|
|
||||||
}
|
|
||||||
ul class="dropdown-menu" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 elemento con contenido HTML arbitrario.
|
|
||||||
///
|
|
||||||
/// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `<li>`
|
|
||||||
/// con las clases de navegación asociadas a [`Item`].
|
|
||||||
pub fn html(html: Html) -> Self {
|
|
||||||
Self {
|
|
||||||
item_kind: ItemKind::Html(Embed::with(html)),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la
|
|
||||||
/// lista de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán
|
|
||||||
/// a su representación en [`Nav`].
|
|
||||||
pub fn dropdown(menu: Dropdown) -> Self {
|
|
||||||
Self {
|
|
||||||
item_kind: ItemKind::Dropdown(Embed::with(menu)),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del elemento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_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,120 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Kind >***************************************************************************************
|
|
||||||
|
|
||||||
/// Define la variante de presentación de un menú [`Nav`](crate::theme::Nav).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Kind {
|
|
||||||
/// Estilo por defecto, lista de enlaces flexible y minimalista.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Pestañas con borde para cambiar entre secciones.
|
|
||||||
Tabs,
|
|
||||||
/// Botones con fondo que resaltan el elemento activo.
|
|
||||||
Pills,
|
|
||||||
/// Variante con subrayado del elemento activo, estética ligera.
|
|
||||||
Underline,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Kind {
|
|
||||||
const TABS: &str = "nav-tabs";
|
|
||||||
const PILLS: &str = "nav-pills";
|
|
||||||
const UNDERLINE: &str = "nav-underline";
|
|
||||||
|
|
||||||
/// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Default => "",
|
|
||||||
Self::Tabs => Self::TABS,
|
|
||||||
Self::Pills => Self::PILLS,
|
|
||||||
Self::Underline => Self::UNDERLINE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade la clase asociada al tipo de menú a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase asociada al tipo de menú, o una cadena vacía si no aplica (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Layout >*************************************************************************************
|
|
||||||
|
|
||||||
/// Distribución y orientación de un menú [`Nav`](crate::theme::Nav).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Layout {
|
|
||||||
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Alinea los elementos al inicio de la fila.
|
|
||||||
Start,
|
|
||||||
/// Centra horizontalmente los elementos.
|
|
||||||
Center,
|
|
||||||
/// Alinea los elementos al final de la fila.
|
|
||||||
End,
|
|
||||||
/// Apila los elementos en columna.
|
|
||||||
Vertical,
|
|
||||||
/// Los elementos se expanden para rellenar la fila.
|
|
||||||
Fill,
|
|
||||||
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
|
|
||||||
Justified,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layout {
|
|
||||||
const START: &str = "justify-content-start";
|
|
||||||
const CENTER: &str = "justify-content-center";
|
|
||||||
const END: &str = "justify-content-end";
|
|
||||||
const VERTICAL: &str = "flex-column";
|
|
||||||
const FILL: &str = "nav-fill";
|
|
||||||
const JUSTIFIED: &str = "nav-justified";
|
|
||||||
|
|
||||||
/// Devuelve la clase base asociada a la distribución y orientación del menú.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Default => "",
|
|
||||||
Self::Start => Self::START,
|
|
||||||
Self::Center => Self::CENTER,
|
|
||||||
Self::End => Self::END,
|
|
||||||
Self::Vertical => Self::VERTICAL,
|
|
||||||
Self::Fill => Self::FILL,
|
|
||||||
Self::Justified => Self::JUSTIFIED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
|
|
||||||
/// aplica (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
//! Definiciones para crear barras de navegación [`Navbar`].
|
|
||||||
//!
|
|
||||||
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
|
|
||||||
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
|
|
||||||
//! [`Nav`](crate::theme::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n).
|
|
||||||
//!
|
|
||||||
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
|
|
||||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
|
||||||
//!
|
|
||||||
//! # Ejemplos
|
|
||||||
//!
|
|
||||||
//! Barra **simple**, sólo con un menú horizontal:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let navbar = Navbar::simple()
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let navbar = Navbar::simple_toggle()
|
|
||||||
//! .with_expand(BreakPoint::MD)
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("PageTop"))
|
|
||||||
//! .with_route(Some(|cx| cx.route("/")));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Tools"))
|
|
||||||
//! .with_item(dropdown::Item::link(
|
|
||||||
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
|
||||||
//! )
|
|
||||||
//! .with_item(dropdown::Item::link(
|
|
||||||
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
|
||||||
//! )
|
|
||||||
//! ))
|
|
||||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("Intranet"))
|
|
||||||
//! .with_route(Some(|cx| cx.route("/")));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_right(brand)
|
|
||||||
//! .with_expand(BreakPoint::LG)
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::pills()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let oc = Offcanvas::new()
|
|
||||||
//! .with_id("main_offcanvas")
|
|
||||||
//! .with_title(L10n::n("Main menu"))
|
|
||||||
//! .with_placement(offcanvas::Placement::Start)
|
|
||||||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::offcanvas(oc)
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
|
||||||
//! .with_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("More"))
|
|
||||||
//! .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
|
||||||
//! .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
|
||||||
//! ))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra **fija arriba**:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("Main App"))
|
|
||||||
//! .with_route(Some(|cx| cx.route("/")));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
|
||||||
//! .with_position(navbar::Position::FixedTop)
|
|
||||||
//! .with_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
|
||||||
//! .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Layout, Position};
|
|
||||||
|
|
||||||
mod brand;
|
|
||||||
pub use brand::Brand;
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Navbar;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::Item;
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
|
||||||
///
|
|
||||||
/// Representa la identidad del sitio con una imagen, título y eslogan:
|
|
||||||
///
|
|
||||||
/// - Si hay URL ([`with_route()`](Self::with_route)), el bloque completo actúa como enlace. Por
|
|
||||||
/// defecto enlaza a la raíz del sitio (`/`).
|
|
||||||
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
|
||||||
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
|
||||||
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Brand {
|
|
||||||
#[getters(skip)]
|
|
||||||
id: AttrId,
|
|
||||||
/// Devuelve la imagen de marca (si la hay).
|
|
||||||
image: Embed<Image>,
|
|
||||||
/// Devuelve el título de la identidad de marca.
|
|
||||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
|
||||||
title: L10n,
|
|
||||||
/// Devuelve el eslogan de la marca.
|
|
||||||
slogan: L10n,
|
|
||||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
|
||||||
#[default(_code = "Some(|cx| cx.route(\"/\"))")]
|
|
||||||
route: Option<FnPathByContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Brand {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
let image = self.image().render(cx);
|
|
||||||
let title = self.title().using(cx);
|
|
||||||
if title.is_empty() && image.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
let slogan = self.slogan().using(cx);
|
|
||||||
Ok(html! {
|
|
||||||
@if let Some(route) = self.route() {
|
|
||||||
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
|
||||||
} @else {
|
|
||||||
span class="navbar-brand" { (image) (title) (slogan) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Brand {
|
|
||||||
// **< Brand BUILDER >**************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_image(mut self, image: Option<Image>) -> Self {
|
|
||||||
self.image.alter_component(image);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el título de la identidad de marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_title(mut self, title: L10n) -> Self {
|
|
||||||
self.title = title;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el eslogan de la marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_slogan(mut self, slogan: L10n) -> Self {
|
|
||||||
self.slogan = slogan;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_route(mut self, route: Option<FnPathByContext>) -> Self {
|
|
||||||
self.route = route;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
|
||||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
|
||||||
|
|
||||||
/// Componente para crear una **barra de navegación**.
|
|
||||||
///
|
|
||||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
|
||||||
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
|
|
||||||
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
|
|
||||||
///
|
|
||||||
/// Ver ejemplos en el módulo [`navbar`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Navbar {
|
|
||||||
#[getters(skip)]
|
|
||||||
id: AttrId,
|
|
||||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
|
||||||
classes: Classes,
|
|
||||||
/// Devuelve el punto de ruptura configurado.
|
|
||||||
expand: BreakPoint,
|
|
||||||
/// Devuelve la disposición configurada para la barra de navegación.
|
|
||||||
layout: navbar::Layout,
|
|
||||||
/// Devuelve la posición configurada para la barra de navegación.
|
|
||||||
position: navbar::Position,
|
|
||||||
/// Devuelve la lista de contenidos.
|
|
||||||
items: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Navbar {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
|
||||||
let mut classes = "navbar".to_string();
|
|
||||||
self.expand().push_class(&mut classes, "navbar-expand", "");
|
|
||||||
self.position().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
// Botón de despliegue (colapso u offcanvas) para la barra.
|
|
||||||
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
|
||||||
let id_content_target = util::join!("#", id_content);
|
|
||||||
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
|
||||||
Some("false")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
html! {
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class="navbar-toggler"
|
|
||||||
data-bs-toggle=(data_bs_toggle)
|
|
||||||
data-bs-target=(id_content_target)
|
|
||||||
aria-controls=(id_content)
|
|
||||||
aria-expanded=[aria_expanded]
|
|
||||||
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
|
|
||||||
{
|
|
||||||
span class="navbar-toggler-icon" {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
|
||||||
let id = cx.required_id::<Self>(self.id(), 1);
|
|
||||||
|
|
||||||
Ok(html! {
|
|
||||||
nav id=(&id) class=[self.classes().get()] {
|
|
||||||
div class="container-fluid" {
|
|
||||||
@match self.layout() {
|
|
||||||
// Barra más sencilla: sólo contenido.
|
|
||||||
navbar::Layout::Simple => {
|
|
||||||
(items)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra sencilla que se puede contraer/expandir.
|
|
||||||
navbar::Layout::SimpleToggle => {
|
|
||||||
@let id_content = util::join!(id, "-content");
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
div id=(&id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda, siempre visible.
|
|
||||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
|
||||||
(brand.render(cx))
|
|
||||||
(items)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda y botón a la derecha.
|
|
||||||
navbar::Layout::BrandLeft(brand) => {
|
|
||||||
@let id_content = util::join!(id, "-content");
|
|
||||||
|
|
||||||
(brand.render(cx))
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
div id=(&id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con botón a la izquierda y marca a la derecha.
|
|
||||||
navbar::Layout::BrandRight(brand) => {
|
|
||||||
@let id_content = util::join!(id, "-content");
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
(brand.render(cx))
|
|
||||||
div id=(&id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
|
||||||
navbar::Layout::Offcanvas(offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
@if let Some(oc) = offcanvas.get() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda y contenido en offcanvas.
|
|
||||||
navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(brand.render(cx))
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
@if let Some(oc) = offcanvas.get() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con contenido en offcanvas y marca a la derecha.
|
|
||||||
navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
(brand.render(cx))
|
|
||||||
@if let Some(oc) = offcanvas.get() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Navbar {
|
|
||||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
|
||||||
pub fn simple() -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::Simple)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
|
|
||||||
pub fn simple_toggle() -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::SimpleToggle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
|
||||||
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
|
||||||
pub fn brand_left(brand: navbar::Brand) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
|
||||||
pub fn brand_right(brand: navbar::Brand) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
|
||||||
pub fn offcanvas(oc: Offcanvas) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::Offcanvas(Embed::with(oc)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
|
||||||
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
|
||||||
Embed::with(brand),
|
|
||||||
Embed::with(oc),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
|
||||||
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
|
||||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
|
||||||
Embed::with(brand),
|
|
||||||
Embed::with(oc),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Navbar BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la barra de navegación.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas a la barra de navegación.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Modificar el color de fondo ([`classes::Background`]).
|
|
||||||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_classes(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
|
|
||||||
self.expand = bp;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el tipo de disposición que tendrá la barra de navegación.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
|
|
||||||
self.layout = layout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define dónde se mostrará la barra de navegación dentro del documento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_position(mut self, position: navbar::Position) -> Self {
|
|
||||||
self.position = position;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
|
||||||
/// barra con una operación [`ChildOp`].
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,ignore
|
|
||||||
/// navbar.with_item(navbar::Item::nav(...));
|
|
||||||
/// navbar.with_item(ChildOp::AddMany(vec![
|
|
||||||
/// navbar::Item::nav(...).into(),
|
|
||||||
/// navbar::Item::text(...).into(),
|
|
||||||
/// ]));
|
|
||||||
/// ```
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
|
||||||
self.items.alter_child(op.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
|
||||||
///
|
|
||||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
|
||||||
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
|
||||||
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
|
||||||
#[derive(AutoDefault, Clone, Debug)]
|
|
||||||
pub enum Item {
|
|
||||||
/// Sin contenido, no produce salida.
|
|
||||||
#[default]
|
|
||||||
Void,
|
|
||||||
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
|
|
||||||
///
|
|
||||||
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
|
||||||
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
|
||||||
/// sólo añade otra dentro del bloque de contenidos.
|
|
||||||
Brand(Embed<navbar::Brand>),
|
|
||||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
|
||||||
Nav(Embed<Nav>),
|
|
||||||
/// Representa un *texto localizado* libre.
|
|
||||||
Text(L10n),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Item {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Void => None,
|
|
||||||
Self::Brand(brand) => brand.id(),
|
|
||||||
Self::Nav(nav) => nav.id(),
|
|
||||||
Self::Text(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
if let Self::Nav(nav) = self {
|
|
||||||
if let Some(mut nav) = nav.get() {
|
|
||||||
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
Ok(match self {
|
|
||||||
Self::Void => html! {},
|
|
||||||
Self::Brand(brand) => html! { (brand.render(cx)) },
|
|
||||||
Self::Nav(nav) => {
|
|
||||||
if let Some(nav) = nav.get() {
|
|
||||||
let items = nav.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
html! {
|
|
||||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Text(text) => html! {
|
|
||||||
span class="navbar-text" {
|
|
||||||
(text.using(cx))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
|
|
||||||
///
|
|
||||||
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
|
||||||
/// desplegable.
|
|
||||||
pub fn brand(brand: navbar::Brand) -> Self {
|
|
||||||
Self::Brand(Embed::with(brand))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
|
||||||
pub fn nav(item: Nav) -> Self {
|
|
||||||
Self::Nav(Embed::with(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
|
||||||
pub fn text(item: L10n) -> Self {
|
|
||||||
Self::Text(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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