Compare commits
No commits in common. "main" and "legacy/simplify-pagetop" have entirely different histories.
main
...
legacy/sim
448 changed files with 14941 additions and 45213 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
|
||||
|
||||
# Archivos de log
|
||||
**/log/*.log*
|
||||
|
||||
# Archivos de configuración locales
|
||||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
.vscode
|
||||
Cargo.lock
|
||||
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.
|
||||
79
CREDITS.md
79
CREDITS.md
|
|
@ -1,40 +1,67 @@
|
|||
# 🔃 Dependencias
|
||||
# 🔃 Dependencies
|
||||
|
||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs/) como son:
|
||||
PageTop is developed in the [Rust programming language](https://www.rust-lang.org/) and stands on
|
||||
the shoulders of true giants, using some of the most stable and renowned libraries (*crates*) from
|
||||
the [Rust ecosystem](https://lib.rs), such as:
|
||||
|
||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
||||
de la aplicación.
|
||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
||||
PageTop y sus extensiones.
|
||||
* [Actix Web](https://actix.rs/) for web services and server management.
|
||||
* [Tracing](https://github.com/tokio-rs/tracing) for the diagnostic system and structured logging.
|
||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates) that incorporate
|
||||
[Fluent](https://projectfluent.org/) for project internationalization.
|
||||
* [SeaORM](https://www.sea-ql.org/SeaORM/) which employs [SQLx](https://docs.rs/sqlx/latest/sqlx/)
|
||||
for database access and modeling.
|
||||
* Among others, which you can review in the PageTop
|
||||
[`Cargo.toml`](https://github.com/manuelcillero/pagetop/blob/main/Cargo.toml) file.
|
||||
|
||||
|
||||
# ⌨️ Code
|
||||
|
||||
PageTop integrates code from various renowned crates to enhance functionality:
|
||||
|
||||
* [**Config (v0.11.0)**](https://github.com/mehcode/config-rs/tree/0.11.0): Includes code from
|
||||
[config-rs](https://crates.io/crates/config) by [Ryan Leckey](https://crates.io/users/mehcode),
|
||||
chosen for its advantages in reading configuration settings and delegating assignment to safe
|
||||
types, tailored to the specific needs of each package, theme, or application.
|
||||
|
||||
* [**Maud (v0.25.0)**](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud): An adapted version
|
||||
of the excellent [maud](https://crates.io/crates/maud) crate by
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) is incorporated to leverage its functionalities without requiring a reference to `maud` in the `Cargo.toml` files.
|
||||
|
||||
* **SmartDefault (v0.7.1)**: Embedded [SmartDefault](https://crates.io/crates/smart_default) by
|
||||
[Jane Doe](https://crates.io/users/jane-doe) as `AutoDefault`to simplify the documentation of
|
||||
Default implementations and also removes the need to explicitly list `smart_default` in the
|
||||
`Cargo.toml` files.
|
||||
|
||||
* **Database Operations**: PageTop employs [SQLx](https://github.com/launchbadge/sqlx) and
|
||||
[SeaQuery](https://github.com/SeaQL/sea-query), complemented by a custom version of
|
||||
[SeaORM Migration](https://github.com/SeaQL/sea-orm/tree/master/sea-orm-migration) (version
|
||||
[0.12.8](https://github.com/SeaQL/sea-orm/tree/0.12.8/sea-orm-migration/src)). This modification
|
||||
ensures migration processes are confined to specific packages, enhancing modularity and
|
||||
maintainability.
|
||||
|
||||
|
||||
# 🗚 FIGfonts
|
||||
|
||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
||||
PageTop uses the [figlet-rs](https://crates.io/crates/figlet-rs) package by *yuanbohan* to display a
|
||||
presentation banner in the terminal with the application's name using
|
||||
[FIGlet](http://www.figlet.org) characters. The fonts included in `src/app` are:
|
||||
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
||||
(predeterminada)
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) de *Claude Martins*
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) by *Glenn Chappell*
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) by *Glenn Chappell* (default)
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) by *Claude Martins*
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) by *Ryan Youck*
|
||||
|
||||
|
||||
# 🎨 CSS
|
||||
# 📰 Templates
|
||||
|
||||
La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/)
|
||||
para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia
|
||||
[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
* The default welcome homepage design is based on the
|
||||
[Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc)
|
||||
template created by [inovatik](https://inovatik.com/) and distributed by
|
||||
[ThemeWagon](https://themewagon.com).
|
||||
|
||||
|
||||
# 👾 Icono
|
||||
# 🎨 Icon
|
||||
|
||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
"The creature" smiling is a fun creation by [Webalys](https://www.iconfinder.com/webalys). It can be
|
||||
found in their [Nasty Icons](https://www.iconfinder.com/iconsets/nasty) collection available on
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
|
|
|
|||
3276
Cargo.lock
generated
3276
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
154
Cargo.toml
154
Cargo.toml
|
|
@ -1,92 +1,90 @@
|
|||
[package]
|
||||
name = "pagetop"
|
||||
version = "0.5.0"
|
||||
version = "0.0.53"
|
||||
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"]
|
||||
description = "An opinionated web framework to build modular Server-Side Rendering web solutions."
|
||||
homepage = "https://pagetop.cillero.es"
|
||||
repository = "https://github.com/manuelcillero/pagetop"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
authors = [
|
||||
"Manuel Cillero <manuel@cillero.es>"
|
||||
]
|
||||
categories = [
|
||||
"web-programming", "gui", "development-tools", "asynchronous"
|
||||
]
|
||||
keywords = [
|
||||
"pagetop", "web", "framework", "frontend", "ssr"
|
||||
]
|
||||
exclude = [
|
||||
"examples/", "helpers/", "tests/"
|
||||
]
|
||||
rust-version = "1.70.0"
|
||||
|
||||
[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"
|
||||
[workspace]
|
||||
members = ["helpers/*"]
|
||||
|
||||
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
|
||||
[lib]
|
||||
name = "pagetop"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testing = []
|
||||
database = ["futures", "sea-orm", "sea-schema"]
|
||||
mysql = ["database", "sea-orm/sqlx-mysql"]
|
||||
postgres = ["database", "sea-orm/sqlx-postgres"]
|
||||
sqlite = ["database", "sea-orm/sqlx-sqlite"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27"
|
||||
serde_json = "1.0"
|
||||
pagetop-aliner.workspace = true
|
||||
pagetop-bootsier.workspace = true
|
||||
[dependencies]
|
||||
async-trait = "0.1.79"
|
||||
chrono = "0.4.37"
|
||||
concat-string = "1.0.1"
|
||||
figlet-rs = "0.1.5"
|
||||
itoa = "1.0.11"
|
||||
nom = "7.1.3"
|
||||
once_cell = "1.19.0"
|
||||
paste = "1.0.14"
|
||||
substring = "1.4.5"
|
||||
term_size = "0.3.2"
|
||||
toml = "0.8.12"
|
||||
url = "2.5.0"
|
||||
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
|
||||
tracing-actix-web = "0.7.10"
|
||||
|
||||
fluent-templates = "0.9.1"
|
||||
unic-langid = { version = "0.9.4", features = ["macros"] }
|
||||
|
||||
actix-web = "4"
|
||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||
|
||||
actix-web-files = { package = "actix-files", version = "0.6.5" }
|
||||
actix-web-static-files = "4.0.1"
|
||||
static-files = "0.2.3"
|
||||
|
||||
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3.30"
|
||||
optional = true
|
||||
|
||||
[dependencies.sea-orm]
|
||||
version = "0.12.15"
|
||||
features = ["debug-print", "macros", "runtime-async-std-native-tls"]
|
||||
default-features = false
|
||||
optional = true
|
||||
|
||||
[dependencies.sea-schema]
|
||||
version = "0.14.2"
|
||||
optional = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
|
||||
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# Helpers
|
||||
"helpers/pagetop-build",
|
||||
"helpers/pagetop-macros",
|
||||
"helpers/pagetop-minimal",
|
||||
"helpers/pagetop-statics",
|
||||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
||||
homepage = "https://pagetop.cillero.es"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-web = { version = "4.13", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# 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 = "." }
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
|
|
|||
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.
|
||||
176
README.md
176
README.md
|
|
@ -1,58 +1,41 @@
|
|||
<div align="center">
|
||||
|
||||
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
|
||||
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
|
||||
|
||||
<h1>PageTop</h1>
|
||||
|
||||
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
|
||||
<p>An opinionated web framework to build modular <em>Server-Side Rendering</em> web solutions.</p>
|
||||
|
||||
[](https://docs.rs/pagetop)
|
||||
[](#-license)
|
||||
[](https://docs.rs/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
||||
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||
según las necesidades de cada proyecto, incluyendo:
|
||||
## Overview
|
||||
|
||||
* **Acciones** (*actions*): alteran la lógica interna de una funcionalidad interceptando su flujo
|
||||
de ejecución.
|
||||
* **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.
|
||||
The PageTop core API provides a comprehensive toolkit for extending its functionalities to specific
|
||||
requirements and application scenarios through actions, components, packages, and themes:
|
||||
|
||||
* **Actions** serve as a mechanism to customize PageTop's internal behavior by intercepting its
|
||||
execution flow.
|
||||
* **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and
|
||||
well-defined units.
|
||||
* **Packages** extend or customize existing functionality by interacting with PageTop APIs or
|
||||
third-party package APIs.
|
||||
* **Themes** enable developers to alter the appearance of pages and components without affecting
|
||||
their functionality.
|
||||
|
||||
|
||||
## ⚡️ Guía rápida
|
||||
# ⚡️ Quick start
|
||||
|
||||
La aplicación más sencilla de PageTop se ve así:
|
||||
|
||||
```rust,no_run
|
||||
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
|
||||
```rust
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Extension for HelloWorld {
|
||||
impl PackageTrait for HelloWorld {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
|
|
@ -60,7 +43,7 @@ impl Extension for HelloWorld {
|
|||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.with_component(Html::with(html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
@ -70,107 +53,42 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
```
|
||||
|
||||
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>`.
|
||||
This program features a `HelloWorld` package, providing a service that serves a greeting web page
|
||||
accessible via `http://localhost:8088` under default settings.
|
||||
|
||||
|
||||
## 📂 Proyecto
|
||||
# 📂 Helpers
|
||||
|
||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||
* [pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros):
|
||||
A collection of procedural macros that enhance the development experience within PageTop.
|
||||
|
||||
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
|
||||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
||||
|
||||
### Auxiliares
|
||||
|
||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
||||
|
||||
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
||||
proporciona una colección de macros procedurales que mejoran la experiencia de desarrollo con
|
||||
PageTop.
|
||||
|
||||
* **[pagetop-minimal](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal)**,
|
||||
ofrece macros declarativas esenciales para optimizar tareas comunes como la composición de
|
||||
texto, la concatenación de cadenas y el manejo de colecciones clave-valor.
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos
|
||||
de forma eficiente, con detección de cambios que optimizan el tiempo de compilación.
|
||||
|
||||
### Extensiones
|
||||
|
||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
||||
|
||||
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
|
||||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
||||
componentes flexibles.
|
||||
* [pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build):
|
||||
Simplifies the process of embedding resources directly into binary files for PageTop applications.
|
||||
|
||||
|
||||
## 🧪 Pruebas
|
||||
# 🚧 Warning
|
||||
|
||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||
|
||||
| Comando | Descripción |
|
||||
| ------- | ----------- |
|
||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
||||
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
|
||||
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
|
||||
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
|
||||
|
||||
> **Nota**
|
||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
**PageTop** framework is currently in active development. The API is unstable and subject to
|
||||
frequent changes. Production use is not recommended until version **0.1.0**.
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
# 📜 License
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
PageTop is free, open source and permissively licensed! Except where noted (below and/or in
|
||||
individual files), all code in this project is dual-licensed under either:
|
||||
|
||||
* MIT License
|
||||
([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
|
||||
|
||||
* Apache License, Version 2.0,
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
at your option. This means you can select the license you prefer! This dual-licensing approach is
|
||||
the de-facto standard in the Rust ecosystem.
|
||||
|
||||
|
||||
## 📜 Licencia
|
||||
# ✨ Contributions
|
||||
|
||||
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*.
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the
|
||||
work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
||||
additional terms or conditions.
|
||||
|
|
|
|||
21
STARTER.bin.Cargo.toml
Normal file
21
STARTER.bin.Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Ver más claves y sus definiciones en
|
||||
# https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Si requiere acceso a base de datos (mysql, postgres y/o sqlite):
|
||||
pagetop = { version = "0.0", features = ["mysql"], default-features = false }
|
||||
# pagetop = "0.0" (en otro caso)
|
||||
|
||||
# Opcional. Para usar archivos y recursos binarios contenidos en el ejecutable:
|
||||
static-files = "0.2.3"
|
||||
# Opcional. Para serializar estructuras de datos:
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
# Opcional. Para incluir archivos y recursos binarios en el ejecutable:
|
||||
pagetop-build = "0.0"
|
||||
21
STARTER.lib.Cargo.toml
Normal file
21
STARTER.lib.Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "module"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# Ver más claves y sus definiciones en
|
||||
# https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Si requiere acceso a base de datos:
|
||||
pagetop = { version = "0.0", features = ["database"], default-features = false }
|
||||
# pagetop = "0.0" (en otro caso)
|
||||
|
||||
# Opcional. Para usar archivos y recursos binarios contenidos en la librería:
|
||||
static-files = "0.2.3"
|
||||
# Opcional. Para serializar estructuras de datos:
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[build-dependencies]
|
||||
# Opcional. Para incluir archivos y recursos binarios en la propia librería:
|
||||
pagetop-build = "0.0"
|
||||
4
build.rs
4
build.rs
|
|
@ -1,7 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("assets")
|
||||
StaticFilesBundle::from_dir("./static/base")
|
||||
.with_name("base")
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
2
config/common.toml
Normal file
2
config/common.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[app]
|
||||
name = "Samples"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[log]
|
||||
tracing = "Info,pagetop=Debug"
|
||||
56
docs/predefined-settings.toml
Normal file
56
docs/predefined-settings.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[app]
|
||||
name = "My App"
|
||||
description = "Developed with the amazing PageTop framework."
|
||||
# Default theme.
|
||||
theme = "Default"
|
||||
# Default language (localization).
|
||||
language = "en-US"
|
||||
# Default text direction: "ltr", "rtl", or "auto".
|
||||
direction = "ltr"
|
||||
# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars".
|
||||
startup_banner = "Slant"
|
||||
|
||||
[database]
|
||||
# Connect to a database (optional).
|
||||
# Database type (mysql, postgres, or sqlite).
|
||||
db_type = ""
|
||||
# Database name (for mysql/postgres) or reference (for sqlite).
|
||||
db_name = ""
|
||||
# User and password (for mysql/postgres).
|
||||
db_user = ""
|
||||
db_pass = ""
|
||||
# Database server (for mysql/postgres).
|
||||
db_host = "localhost"
|
||||
# Port, usually 3306 (for mysql) or 5432 (for postgres).
|
||||
db_port = 0
|
||||
# Maximum number of enabled connections.
|
||||
max_pool_size = 5
|
||||
|
||||
[dev]
|
||||
# Static files required by the app are integrated by default into the executable
|
||||
# binary. However, during development, it can be useful to serve these files
|
||||
# from their own directory to avoid recompiling every time they are modified. In
|
||||
# this case, just indicate the full path to the project's root directory.
|
||||
pagetop_project_dir = ""
|
||||
|
||||
[log]
|
||||
# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace".
|
||||
# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
||||
tracing = "Info"
|
||||
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
|
||||
rolling = "Stdout"
|
||||
# Directory for trace files (if rolling != "Stdout").
|
||||
path = "log"
|
||||
# Prefix for trace files (if rolling != "Stdout").
|
||||
prefix = "tracing.log"
|
||||
# Traces format: "Full", "Compact", "Pretty", or "Json".
|
||||
format = "Full"
|
||||
|
||||
[server]
|
||||
# Web server config.
|
||||
bind_address = "localhost"
|
||||
bind_port = 8088
|
||||
# Session cookie duration (in seconds), i.e., the time from when the session is
|
||||
# created until the cookie expires. A value of 0 indicates "until the browser is
|
||||
# closed". By default, it is one week.
|
||||
session_lifetime = 604800
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,23 +2,20 @@ use pagetop::prelude::*;
|
|||
|
||||
struct HelloName;
|
||||
|
||||
impl Extension for HelloName {
|
||||
impl PackageTrait for HelloName {
|
||||
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(
|
||||
request: HttpRequest,
|
||||
path: service::web::Path<String>,
|
||||
) -> ResultPage<Markup, ErrorPage> {
|
||||
let name = path.into_inner();
|
||||
Page::new(request)
|
||||
.with_child(Html::with(move |_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello " (name) "!" }
|
||||
}
|
||||
}))
|
||||
.with_component(Html::with(html! { h1 { "Hello " (name) "!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use pagetop::prelude::*;
|
|||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Extension for HelloWorld {
|
||||
impl PackageTrait for HelloWorld {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
|
|
@ -10,11 +10,7 @@ impl Extension for HelloWorld {
|
|||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(Html::with(|_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello World!" }
|
||||
}
|
||||
}))
|
||||
.with_component(Html::with(html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,7 +0,0 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("aliner")
|
||||
.build()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
#[default]
|
||||
Simple,
|
||||
|
||||
/// Barra simple, con botón de despliegue a la izquierda y sin marca de identidad.
|
||||
SimpleToggle,
|
||||
|
||||
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
SimpleBrandLeft(Embed<navbar::Brand>),
|
||||
|
||||
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
||||
BrandLeft(Embed<navbar::Brand>),
|
||||
|
||||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
||||
BrandRight(Embed<navbar::Brand>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
||||
Offcanvas(Embed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
||||
/// derecha.
|
||||
OffcanvasBrandLeft(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
||||
/// derecha.
|
||||
OffcanvasBrandRight(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||
}
|
||||
|
||||
// **< Position >***********************************************************************************
|
||||
|
||||
/// Posición global de una barra de navegación [`Navbar`] en el documento.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Position {
|
||||
/// Barra normal, fluye con el documento.
|
||||
#[default]
|
||||
Static,
|
||||
/// Barra fijada en la parte superior, siempre visible.
|
||||
///
|
||||
/// Puede ser necesario reservar espacio en la parte superior del contenido que fluye debajo
|
||||
/// para evitar que quede oculto por la barra.
|
||||
FixedTop,
|
||||
/// Barra fijada en la parte inferior, siempre visible.
|
||||
///
|
||||
/// Puede ser necesario reservar espacio en la parte inferior del contenido que fluye debajo
|
||||
/// para evitar que quede oculto por la barra.
|
||||
FixedBottom,
|
||||
/// La barra de navegación se fija en la parte superior al hacer *scroll*.
|
||||
StickyTop,
|
||||
/// La barra de navegación se fija en la parte inferior al hacer *scroll*.
|
||||
StickyBottom,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Devuelve la clase base asociada a la posición de la barra de navegación.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Static => "",
|
||||
Self::FixedTop => "fixed-top",
|
||||
Self::FixedBottom => "fixed-bottom",
|
||||
Self::StickyTop => "sticky-top",
|
||||
Self::StickyBottom => "sticky-bottom",
|
||||
}
|
||||
}
|
||||
|
||||
/// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
|
||||
/// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_string()
|
||||
} */
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let panel = Offcanvas::new()
|
||||
//! .with_id("offcanvas_example")
|
||||
//! .with_title(L10n::n("Offcanvas title"))
|
||||
//! .with_placement(offcanvas::Placement::End)
|
||||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||
//! .with_visibility(offcanvas::Visibility::Default)
|
||||
//! .with_child(Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .with_item(dropdown::Item::label(L10n::n("Label")))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
||||
|
||||
mod component;
|
||||
pub use component::Offcanvas;
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales.
|
||||
///
|
||||
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
||||
/// características principales:
|
||||
///
|
||||
/// - Puede mostrar una capa de fondo para centrar la atención del usuario en el panel
|
||||
/// ([`with_backdrop()`](Self::with_backdrop)); o puede bloquear el desplazamiento del documento
|
||||
/// principal ([`with_body_scroll()`](Self::with_body_scroll)).
|
||||
/// - Se puede configurar el borde de la ventana desde el que se desliza el panel
|
||||
/// ([`with_placement()`](Self::with_placement)).
|
||||
/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado.
|
||||
/// - Puede cambiar su comportamiento a partir de un punto de ruptura
|
||||
/// ([`with_breakpoint()`](Self::with_breakpoint)).
|
||||
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
|
||||
/// adecuados para lectores de pantalla y navegación por teclado.
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Offcanvas {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al panel.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del panel.
|
||||
title: L10n,
|
||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
||||
breakpoint: BreakPoint,
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
backdrop: offcanvas::Backdrop,
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
body_scroll: offcanvas::BodyScroll,
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
placement: offcanvas::Placement,
|
||||
/// Devuelve el estado inicial del panel.
|
||||
visibility: offcanvas::Visibility,
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Offcanvas {
|
||||
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 = "offcanvas".to_string();
|
||||
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
||||
self.placement().push_class(&mut classes);
|
||||
self.visibility().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.render_offcanvas(cx, None))
|
||||
}
|
||||
}
|
||||
|
||||
impl Offcanvas {
|
||||
// **< Offcanvas BUILDER >**********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del panel.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al panel.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el título del encabezado.
|
||||
#[builder_fn]
|
||||
pub fn with_title(mut self, title: L10n) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel.
|
||||
///
|
||||
/// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante
|
||||
/// ([`Offcanvas`]).
|
||||
/// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página.
|
||||
///
|
||||
/// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible
|
||||
/// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea
|
||||
/// *offcanvas* siempre.
|
||||
#[builder_fn]
|
||||
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
|
||||
self.breakpoint = bp;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
|
||||
/// panel.
|
||||
#[builder_fn]
|
||||
pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self {
|
||||
self.backdrop = backdrop;
|
||||
self
|
||||
}
|
||||
|
||||
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
|
||||
#[builder_fn]
|
||||
pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self {
|
||||
self.body_scroll = scrolling;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indica desde qué borde de la ventana entra y se ancla el panel.
|
||||
#[builder_fn]
|
||||
pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self {
|
||||
self.placement = placement;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fija el estado inicial del panel (oculto o visible al cargar).
|
||||
#[builder_fn]
|
||||
pub fn with_visibility(mut self, visibility: offcanvas::Visibility) -> Self {
|
||||
self.visibility = visibility;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al panel 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
|
||||
}
|
||||
|
||||
// **< Offcanvas HELPERS >**********************************************************************
|
||||
|
||||
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
|
||||
let body = self.children().render(cx);
|
||||
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {});
|
||||
if body.is_empty() && body_extra.is_empty() {
|
||||
return html! {};
|
||||
}
|
||||
|
||||
let id = cx.required_id::<Self>(self.id(), 1);
|
||||
let id_label = util::join!(id, "-label");
|
||||
let id_target = util::join!("#", id);
|
||||
|
||||
let body_scroll = match self.body_scroll() {
|
||||
offcanvas::BodyScroll::Disabled => None,
|
||||
offcanvas::BodyScroll::Enabled => Some("true"),
|
||||
};
|
||||
|
||||
let backdrop = match self.backdrop() {
|
||||
offcanvas::Backdrop::Disabled => Some("false"),
|
||||
offcanvas::Backdrop::Enabled => None,
|
||||
offcanvas::Backdrop::Static => Some("static"),
|
||||
};
|
||||
|
||||
let title = self.title().using(cx);
|
||||
|
||||
html! {
|
||||
div
|
||||
id=(&id)
|
||||
class=[self.classes().get()]
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[body_scroll]
|
||||
data-bs-backdrop=[backdrop]
|
||||
aria-labelledby=(id_label)
|
||||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 id=(&id_label) class="offcanvas-title" { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="offcanvas"
|
||||
data-bs-target=(id_target)
|
||||
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{}
|
||||
}
|
||||
div class="offcanvas-body" {
|
||||
(body)
|
||||
(body_extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< Backdrop >***********************************************************************************
|
||||
|
||||
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
|
||||
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Backdrop {
|
||||
/// Sin capa de fondo, la página principal permanece visible e interactiva.
|
||||
Disabled,
|
||||
/// Opción por defecto, se oscurece el fondo; un clic fuera del panel suele cerrarlo.
|
||||
#[default]
|
||||
Enabled,
|
||||
/// Muestra la capa de fondo pero no se cierra al hacer clic fuera del panel. Útil si se
|
||||
/// requiere completar una acción antes de salir.
|
||||
Static,
|
||||
}
|
||||
|
||||
// **< BodyScroll >*********************************************************************************
|
||||
|
||||
/// Controla si la página principal puede desplazarse al abrir un panel
|
||||
/// [`Offcanvas`](crate::theme::Offcanvas).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum BodyScroll {
|
||||
/// Opción por defecto, la página principal se bloquea centrando la interacción en el panel.
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Permite el desplazamiento de la página principal.
|
||||
Enabled,
|
||||
}
|
||||
|
||||
// **< Placement >**********************************************************************************
|
||||
|
||||
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
||||
///
|
||||
/// Define desde qué borde de la ventana entra y se ancla el panel.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Placement {
|
||||
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
|
||||
#[default]
|
||||
Start,
|
||||
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
|
||||
End,
|
||||
/// Desde la parte superior.
|
||||
Top,
|
||||
/// Desde la parte inferior.
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
/// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Placement::Start => "offcanvas-start",
|
||||
Placement::End => "offcanvas-end",
|
||||
Placement::Top => "offcanvas-top",
|
||||
Placement::Bottom => "offcanvas-bottom",
|
||||
}
|
||||
}
|
||||
|
||||
/// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(self.as_str());
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de aparición del panel (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
|
||||
// **< Visibility >*********************************************************************************
|
||||
|
||||
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Visibility {
|
||||
/// El panel permanece oculto desde el principio.
|
||||
#[default]
|
||||
Default,
|
||||
/// El panel se muestra abierto al cargar.
|
||||
Show,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
/// Devuelve la clase base asociada al estado inicial del panel.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Visibility::Default => "",
|
||||
Visibility::Show => "show",
|
||||
}
|
||||
}
|
||||
|
||||
/// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
if class.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/* Devuelve la clase asociada al estado inicial, o una cadena vacía si no aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
} */
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
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