Compare commits
No commits in common. "main" and "legacy/latest" have entirely different histories.
main
...
legacy/lat
709 changed files with 27867 additions and 20898 deletions
|
|
@ -1,69 +0,0 @@
|
||||||
# cliff.toml
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
header = """
|
|
||||||
# CHANGELOG
|
|
||||||
|
|
||||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
|
||||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
|
||||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
|
||||||
|
|
||||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
|
||||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
|
||||||
internos pueden omitirse si no afectan al uso del proyecto.
|
|
||||||
"""
|
|
||||||
trim = true
|
|
||||||
render_always = true
|
|
||||||
|
|
||||||
body = """
|
|
||||||
{% if version %}
|
|
||||||
## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }})
|
|
||||||
{% else %}
|
|
||||||
## Pendiente de publicación
|
|
||||||
{% endif %}\
|
|
||||||
{% for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | upper_first }}
|
|
||||||
|
|
||||||
{% for commit in commits %}
|
|
||||||
{%- set msg = commit.message
|
|
||||||
| split(pat="\n")
|
|
||||||
| first
|
|
||||||
| replace(from="✨ ", to="")
|
|
||||||
| replace(from="🐛 ", to="")
|
|
||||||
| replace(from="🚑 ", to="")
|
|
||||||
| replace(from="⬆️ ", to="")
|
|
||||||
| replace(from="🚧 ", to="")
|
|
||||||
| replace(from="♻️ ", to="")
|
|
||||||
| replace(from="✏️ ", to="")
|
|
||||||
| replace(from="🏷️ ", to="")
|
|
||||||
| replace(from="🧑💻 ", to="")
|
|
||||||
| replace(from="🍱 ", to="")
|
|
||||||
| replace(from="📝 ", to="")
|
|
||||||
| replace(from="💡 ", to="")
|
|
||||||
-%}
|
|
||||||
|
|
||||||
- {{ msg | trim }} {% if commit.author.name != "Manuel Cillero" %} - {{ commit.author.name }}{% endif %}
|
|
||||||
{% endfor %}{% endfor %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
[git]
|
|
||||||
conventional_commits = false
|
|
||||||
filter_unconventional = false
|
|
||||||
topo_order_commits = true
|
|
||||||
sort_commits = "oldest"
|
|
||||||
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^✨", group = "Añadido" },
|
|
||||||
{ message = "^🐛", group = "Corregido" },
|
|
||||||
{ message = "^🚑", group = "Corregido" },
|
|
||||||
{ message = "^🚧", group = "Cambiado" },
|
|
||||||
{ message = "^♻️", group = "Cambiado" },
|
|
||||||
{ message = "^✏️", group = "Cambiado" },
|
|
||||||
{ message = "^🏷️", group = "Cambiado" },
|
|
||||||
{ message = "^🧑💻", group = "Cambiado" },
|
|
||||||
{ message = "^🍱", group = "Cambiado" },
|
|
||||||
{ message = "^⬆️", group = "Dependencias" },
|
|
||||||
{ message = "^📝", group = "Documentado" },
|
|
||||||
{ message = "^💡", group = "Documentado" },
|
|
||||||
{ message = "^.*", group = "Otros cambios" },
|
|
||||||
]
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,3 +8,6 @@
|
||||||
**/local.*.toml
|
**/local.*.toml
|
||||||
**/local.toml
|
**/local.toml
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Directorio de trabajo
|
||||||
|
workdir
|
||||||
|
|
|
||||||
86
CHANGELOG.md
86
CHANGELOG.md
|
|
@ -1,86 +0,0 @@
|
||||||
# CHANGELOG
|
|
||||||
|
|
||||||
Este archivo documenta los cambios más relevantes realizados en cada versión. El formato está basado
|
|
||||||
en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y las versiones se numeran siguiendo
|
|
||||||
las reglas del [Versionado Semántico](https://semver.org/lang/es/).
|
|
||||||
|
|
||||||
Resume la evolución del proyecto para usuarios y colaboradores, destacando nuevas funcionalidades,
|
|
||||||
correcciones, mejoras durante el desarrollo o cambios en la documentación. Cambios menores o
|
|
||||||
internos pueden omitirse si no afectan al uso del proyecto.
|
|
||||||
|
|
||||||
## 0.4.0 (2025-09-20)
|
|
||||||
|
|
||||||
### Añadido
|
|
||||||
|
|
||||||
- [app] Añade manejo de rutas no encontradas
|
|
||||||
- [context] Añade métodos auxiliares de parámetros
|
|
||||||
- [util] Añade `indoc` para indentar código bien
|
|
||||||
- Añade componente `PoweredBy` para copyright
|
|
||||||
|
|
||||||
### Cambiado
|
|
||||||
|
|
||||||
- [html] Cambia tipos `Option...` por `Attr...`
|
|
||||||
- [html] Implementa `Default` en `Context`
|
|
||||||
- [welcome] Crea página de bienvenida desde intro
|
|
||||||
- [context] Generaliza los parámetros de contexto
|
|
||||||
- [context] Define un `trait` común de contexto
|
|
||||||
- Modifica tipos para atributos HTML a minúsculas
|
|
||||||
- Renombra `with_component` por `add_child`
|
|
||||||
|
|
||||||
### Corregido
|
|
||||||
|
|
||||||
- [welcome] Corrige giro botón con ancho estrecho
|
|
||||||
- [welcome] Corrige centrado del pie de página
|
|
||||||
- Corrige nombre de función en prueba de `Html`
|
|
||||||
- Corrige doc y código por cambios en Page
|
|
||||||
|
|
||||||
### Dependencias
|
|
||||||
|
|
||||||
- Actualiza dependencias para 0.4.0
|
|
||||||
|
|
||||||
### Documentado
|
|
||||||
|
|
||||||
- [component] Amplía documentación de preparación
|
|
||||||
- Normaliza referencias al nombre PageTop
|
|
||||||
- Simplifica documentación de obsoletos
|
|
||||||
- Mejora la documentación de recursos y contexto
|
|
||||||
|
|
||||||
### Otros cambios
|
|
||||||
|
|
||||||
- 🎨 [theme] Mejora gestión de regiones en páginas
|
|
||||||
- ✅ [tests] Amplía pruebas para `PrepareMarkup'
|
|
||||||
- 🎨 [locale] Mejora el uso de `lookup` / `using`
|
|
||||||
- 🔨 [tools] Fuerza pulsar intro para confirmar input
|
|
||||||
- 💄 Aplica BEM a estilos de bienvenida y componente
|
|
||||||
- 🎨 Unifica conversiones a String con `to_string()`
|
|
||||||
- 🔥 Elimina `Render` para usar siempre el contexto
|
|
||||||
|
|
||||||
## 0.3.0 (2025-08-16)
|
|
||||||
|
|
||||||
### Cambiado
|
|
||||||
|
|
||||||
- Redefine función para directorios absolutos
|
|
||||||
- Mejora la integración de archivos estáticos
|
|
||||||
|
|
||||||
### Documentado
|
|
||||||
|
|
||||||
- Cambia el formato para la documentación
|
|
||||||
|
|
||||||
## 0.2.0 (2025-08-09)
|
|
||||||
|
|
||||||
### Añadido
|
|
||||||
|
|
||||||
- Añade librería para gestionar recursos estáticos
|
|
||||||
- Añade soporte a changelog de `pagetop-statics`
|
|
||||||
|
|
||||||
### Documentado
|
|
||||||
|
|
||||||
- Corrige enlace del botón de licencia en la documentación
|
|
||||||
|
|
||||||
### Otros cambios
|
|
||||||
|
|
||||||
- Afina Cargo.toml para buscar la mejor categoría
|
|
||||||
|
|
||||||
## 0.1.0 (2025-08-06)
|
|
||||||
|
|
||||||
- Versión inicial
|
|
||||||
32
CREDITS.md
32
CREDITS.md
|
|
@ -1,23 +1,31 @@
|
||||||
# 🔃 Dependencias
|
# 🔃 Dependencias
|
||||||
|
|
||||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
`PageTop` está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes
|
||||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
|
aprovechando algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs)
|
||||||
|
como:
|
||||||
|
|
||||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
* [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
|
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
||||||
de la aplicación.
|
de la aplicación.
|
||||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
||||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
||||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
* Además de otros crates adicionales que puedes explorar en los archivos `Cargo.toml` de `PageTop`
|
||||||
PageTop y sus extensiones.
|
y sus extensiones.
|
||||||
|
|
||||||
|
|
||||||
|
# ⌨️ Código
|
||||||
|
|
||||||
|
`PageTop` incorpora código de [config-rs](https://crates.io/crates/config) (versión
|
||||||
|
[0.11.0](https://github.com/mehcode/config-rs/tree/0.11.0)) de
|
||||||
|
[Ryan Leckey](https://crates.io/users/mehcode), por sus ventajas para leer y asignar a tipos seguros
|
||||||
|
las opciones de configuración, delegando la asignación a cada extensión, tema o aplicación.
|
||||||
|
|
||||||
|
|
||||||
# 🗚 FIGfonts
|
# 🗚 FIGfonts
|
||||||
|
|
||||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
`PageTop` usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por
|
||||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
*yuanbohan* para mostrar un banner de presentación en el terminal con el nombre de la aplicación en
|
||||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
caracteres [FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
||||||
|
|
||||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
|
* [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*
|
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
||||||
|
|
@ -26,6 +34,14 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic
|
||||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||||
|
|
||||||
|
|
||||||
|
# 📰 Plantillas
|
||||||
|
|
||||||
|
La página de inicio predeterminada está inspirada en este práctico
|
||||||
|
[tutorial](https://www.codewithfaraz.com/content/109/creating-a-unique-neobrutalism-portfolio-page-with-html-css-and-javascript)
|
||||||
|
realizado por [Faraz](https://www.codewithfaraz.com/) que crea una página de demostración en estilo
|
||||||
|
*Neobrutalismo*.
|
||||||
|
|
||||||
|
|
||||||
# 🎨 Icono
|
# 🎨 Icono
|
||||||
|
|
||||||
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||||
|
|
|
||||||
2932
Cargo.lock
generated
2932
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
102
Cargo.toml
102
Cargo.toml
|
|
@ -1,90 +1,50 @@
|
||||||
[package]
|
|
||||||
name = "pagetop"
|
|
||||||
version = "0.4.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
description = """
|
|
||||||
Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
|
|
||||||
"""
|
|
||||||
categories = ["web-programming::http-server"]
|
|
||||||
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
|
|
||||||
|
|
||||||
repository.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chrono = "0.4"
|
|
||||||
colored = "3.0"
|
|
||||||
concat-string = "1.0"
|
|
||||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
|
||||||
figlet-rs = "0.1"
|
|
||||||
indoc = "2.0"
|
|
||||||
itoa = "1.0"
|
|
||||||
parking_lot = "0.12"
|
|
||||||
paste = { package = "pastey", version = "0.1" }
|
|
||||||
substring = "1.4"
|
|
||||||
terminal_size = "0.4"
|
|
||||||
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-appender = "0.2"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
|
||||||
tracing-actix-web = "0.7"
|
|
||||||
|
|
||||||
fluent-templates = "0.13"
|
|
||||||
unic-langid = { version = "0.9", features = ["macros"] }
|
|
||||||
|
|
||||||
actix-web = { workspace = true, default-features = true }
|
|
||||||
actix-session = { version = "0.11", features = ["cookie-session"] }
|
|
||||||
actix-web-files = { package = "actix-files", version = "0.6" }
|
|
||||||
|
|
||||||
serde.workspace = true
|
|
||||||
|
|
||||||
pagetop-macros.workspace = true
|
|
||||||
pagetop-statics.workspace = true
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
testing = []
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3.23"
|
|
||||||
serde_json = "1.0"
|
|
||||||
pagetop-aliner.workspace = true
|
|
||||||
pagetop-bootsier.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
pagetop-build.workspace = true
|
|
||||||
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
# Helpers
|
# Helpers
|
||||||
"helpers/pagetop-build",
|
"helpers/pagetop-build",
|
||||||
"helpers/pagetop-macros",
|
"helpers/pagetop-macros",
|
||||||
"helpers/pagetop-statics",
|
|
||||||
|
# PageTop
|
||||||
|
"pagetop",
|
||||||
|
|
||||||
# Extensions
|
# Extensions
|
||||||
"extensions/pagetop-aliner",
|
"extensions/pagetop-seaorm",
|
||||||
|
"extensions/pagetop-mdbook",
|
||||||
|
"extensions/pagetop-hljs",
|
||||||
|
|
||||||
|
# Themes
|
||||||
|
#"extensions/pagetop-aliner",
|
||||||
"extensions/pagetop-bootsier",
|
"extensions/pagetop-bootsier",
|
||||||
|
|
||||||
|
# Apps
|
||||||
|
"website",
|
||||||
|
"drust",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
repository = "https://github.com/manuelcillero/pagetop"
|
||||||
homepage = "https://pagetop.cillero.es"
|
homepage = "https://pagetop.cillero.es"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-web = { version = "4.11", default-features = false }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
static-files = "0.2.4"
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
pagetop-build = { version = "0.0", path = "helpers/pagetop-build" }
|
||||||
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
|
pagetop-macros = { version = "0.0", path = "helpers/pagetop-macros" }
|
||||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
|
||||||
# Extensions
|
|
||||||
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
|
||||||
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }
|
|
||||||
# PageTop
|
# PageTop
|
||||||
pagetop = { version = "0.4", path = "." }
|
pagetop = { version = "0.0", path = "pagetop" }
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||||
|
pagetop-mdbook = { version = "0.0", path = "extensions/pagetop-mdbook" }
|
||||||
|
pagetop-hljs = { version = "0.0", path = "extensions/pagetop-hljs" }
|
||||||
|
|
||||||
|
# Themes
|
||||||
|
#pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
||||||
|
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }
|
||||||
|
|
|
||||||
98
README.md
98
README.md
|
|
@ -1,21 +1,20 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
|
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
|
||||||
|
|
||||||
<h1>PageTop</h1>
|
<h1>PageTop</h1>
|
||||||
|
|
||||||
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
|
<p>Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.</p>
|
||||||
|
|
||||||
|
[](#-license)
|
||||||
[](https://docs.rs/pagetop)
|
[](https://docs.rs/pagetop)
|
||||||
[](https://crates.io/crates/pagetop)
|
[](https://crates.io/crates/pagetop)
|
||||||
[](https://crates.io/crates/pagetop)
|
[](https://crates.io/crates/pagetop)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
`PageTop` reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para
|
||||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
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
|
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||||
según las necesidades de cada proyecto, incluyendo:
|
según las necesidades de cada proyecto, incluyendo:
|
||||||
|
|
||||||
|
|
@ -24,16 +23,16 @@ según las necesidades de cada proyecto, incluyendo:
|
||||||
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
||||||
configurables y reutilizables.
|
configurables y reutilizables.
|
||||||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
||||||
de PageTop o de terceros.
|
de `PageTop` o de terceros.
|
||||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||||
componentes sin comprometer su funcionalidad.
|
componentes sin comprometer su funcionalidad.
|
||||||
|
|
||||||
|
|
||||||
# ⚡️ Guía rápida
|
# ⚡️ Guía rápida
|
||||||
|
|
||||||
La aplicación más sencilla de PageTop se ve así:
|
La aplicación más sencilla de `PageTop` se ve así:
|
||||||
|
|
||||||
```rust,no_run
|
```rust#ignore
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
|
|
@ -42,17 +41,17 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
Por defecto, este código sirve una página web de bienvenida accesible desde un navegador en la
|
||||||
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
|
dirección `http://localhost:8088`, siguiendo la configuración predeterminada.
|
||||||
|
|
||||||
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
|
Para personalizar el servicio, puedes crear una extensión de `PageTop` de la siguiente manera:
|
||||||
|
|
||||||
```rust,no_run
|
```rust#ignore
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
impl Extension for HelloWorld {
|
impl ExtensionTrait for HelloWorld {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||||
scfg.route("/", service::web::get().to(hello_world));
|
scfg.route("/", service::web::get().to(hello_world));
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +59,7 @@ impl Extension for HelloWorld {
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.with_component(Html::with(html! { h1 { "Hello world!" } }))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,58 +75,57 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
|
||||||
|
|
||||||
# 📂 Repositorio
|
# 📂 Repositorio
|
||||||
|
|
||||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
El código se organiza en un *workspace* con los siguientes subproyectos:
|
||||||
|
|
||||||
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
|
* **[pagetop](https://github.com/manuelcillero/pagetop/tree/latest/pagetop)**, es la librería
|
||||||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
principal. Reúne algunos de los *crates* más estables y populares del ecosistema Rust para
|
||||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
||||||
|
|
||||||
## Auxiliares
|
## Auxiliares
|
||||||
|
|
||||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
* **[pagetop-build](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-build)**,
|
||||||
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
|
permite incluir fácilmente archivos estáticos o archivos SCSS compilados directamente en el
|
||||||
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
|
binario de las aplicaciones `PageTop`.
|
||||||
compilación.
|
|
||||||
|
|
||||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
* **[pagetop-macros](https://github.com/manuelcillero/pagetop/tree/latest/helpers/pagetop-macros)**,
|
||||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
proporciona una colección de macros que mejoran la experiencia de desarrollo con `PageTop`.
|
||||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
|
||||||
|
|
||||||
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
|
||||||
proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
|
||||||
|
|
||||||
## Extensiones
|
## Extensiones
|
||||||
|
|
||||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
* **[pagetop-seaorm](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-seaorm)**,
|
||||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
integra [SeaORM](https://www.sea-ql.org/SeaORM) para trabajar con bases de datos en aplicaciones
|
||||||
|
`PageTop`.
|
||||||
|
|
||||||
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
|
* **[pagetop-mdbook](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-mdbook)**,
|
||||||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
incluye contenido generado por [mdBook](https://rust-lang.github.io/mdBook/) en aplicaciones
|
||||||
componentes flexibles.
|
desarrolladas con `PageTop`.
|
||||||
|
|
||||||
|
* **[pagetop-hljs](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-hljs)**,
|
||||||
|
utiliza [HighlightJS](https://highlightjs.org) para mostrar fragmentos de código con resaltado
|
||||||
|
de sintaxis con `PageTop`.
|
||||||
|
|
||||||
# 🧪 Pruebas
|
## Temas
|
||||||
|
|
||||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
* **[pagetop-bootsier](https://github.com/manuelcillero/pagetop/tree/latest/extensions/pagetop-bootsier)**,
|
||||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
tema para `PageTop` que usa [Bootstrap](https://getbootstrap.com) para dar vida a tus diseños
|
||||||
|
web.
|
||||||
|
|
||||||
| Comando | Descripción |
|
## Aplicaciones
|
||||||
| ------- | ----------- |
|
|
||||||
| `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**
|
* **[drust](https://github.com/manuelcillero/pagetop/tree/latest/drust)**, es una aplicación que
|
||||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
utiliza `PageTop` para crear un Sistema de Gestión de Contenidos (CMS) que permita construir
|
||||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
sitios web dinámicos, administrados y configurables.
|
||||||
|
|
||||||
|
* **[website](https://github.com/manuelcillero/pagetop/tree/latest/website)**, es la aplicación
|
||||||
|
web creada con el propio entorno `PageTop` para descubrir a la comunidad su ecosistema en
|
||||||
|
[pagetop.cillero.es](https://pagetop.cillero.es).
|
||||||
|
|
||||||
|
|
||||||
# 🚧 Advertencia
|
# 🚧 Advertencia
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Su API es
|
||||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
inestable y está sujeta a cambios frecuentes. No recomiendo su uso en producción, al menos mientras
|
||||||
hasta que se libere la versión **1.0.0**.
|
no se libere una versión **1.0.0**.
|
||||||
|
|
||||||
|
|
||||||
# 📜 Licencia
|
# 📜 Licencia
|
||||||
|
|
@ -144,7 +142,7 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
|
||||||
el ecosistema Rust.
|
el ecosistema Rust.
|
||||||
|
|
||||||
|
|
||||||
# ✨ Contribuir
|
# ✨ Contribuciones
|
||||||
|
|
||||||
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
|
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
|
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[log]
|
|
||||||
tracing = "Info,pagetop=Debug"
|
|
||||||
22
drust/Cargo.toml
Normal file
22
drust/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "drust"
|
||||||
|
version = "0.0.5"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
description = """\
|
||||||
|
Un Sistema de Gestión de Contenidos (CMS) basado en PageTop para compartir tu mundo.\
|
||||||
|
"""
|
||||||
|
default-run = "drust"
|
||||||
|
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pagetop.workspace = true
|
||||||
|
pagetop-bootsier.workspace = true
|
||||||
|
|
||||||
|
#pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
|
||||||
|
#pagetop-user = { version = "0.0", path = "../pagetop-user" }
|
||||||
|
#pagetop-node = { version = "0.0", path = "../pagetop-node" }
|
||||||
112
drust/README.md
Normal file
112
drust/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h1>Drust</h1>
|
||||||
|
|
||||||
|
<p>Un Sistema de Gestión de Contenidos (CMS) basado en <strong>PageTop</strong> para compartir tu mundo.</p>
|
||||||
|
|
||||||
|
[](#-license)
|
||||||
|
[](https://crates.io/crates/drust)
|
||||||
|
[](https://crates.io/crates/drust)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Descripción general
|
||||||
|
|
||||||
|
`Drust` exprime `PageTop` para desarrollar un *Sistema de Gestión de Contenidos* (CMS) básico,
|
||||||
|
modestamente inspirado en [Drupal](https://www.drupal.org), que permita construir sitios web
|
||||||
|
dinámicos, manejables y personalizables; y facilite a los usuarios la gestión de una variedad de
|
||||||
|
contenidos de manera sencilla.
|
||||||
|
|
||||||
|
## Sobre PageTop
|
||||||
|
|
||||||
|
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||||
|
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||||
|
configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
|
|
||||||
|
# ⚡️ Guía rápida
|
||||||
|
|
||||||
|
`Drust` requiere una base de datos para funcionar. La aplicación se encarga de ejecutar las
|
||||||
|
migraciones y cargar los datos mínimos necesarios, pero para crear o borrar la base de datos puedes
|
||||||
|
usar los scripts `db-create.sh` y `db-delete.sh` que se encuentran en el directorio `tools` del
|
||||||
|
*workspace*.
|
||||||
|
|
||||||
|
## Configuración de `.env`
|
||||||
|
|
||||||
|
Para simplificar la configuración, en el directorio `tools` puedes crear un archivo `.env` para
|
||||||
|
definir las variables de entorno que requieren los scripts para gestionar la base de datos, aunque
|
||||||
|
su presencia es **opcional**. Si no se encuentra `.env` o carece de ciertos valores, los scripts
|
||||||
|
solicitarán las variables necesarias para su ejecución.
|
||||||
|
|
||||||
|
> **Nota**: Evita usar caracteres especiales como `@`, `#`, `?`, `:` en `DB_PASS` para prevenir
|
||||||
|
> posibles problemas de interpretación de `DATABASE_URL` en el código.
|
||||||
|
|
||||||
|
### Ejemplo de `.env`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sistema de base de datos
|
||||||
|
DB_SYSTEM="psql"
|
||||||
|
|
||||||
|
# Nombre del host
|
||||||
|
DB_HOST="localhost"
|
||||||
|
|
||||||
|
# Puerto de conexión
|
||||||
|
DB_PORT="5432"
|
||||||
|
|
||||||
|
# Nombre de la base de datos
|
||||||
|
DB_NAME="drust"
|
||||||
|
|
||||||
|
# Usuario de la base de datos
|
||||||
|
DB_USER="drust"
|
||||||
|
|
||||||
|
# Contraseña para el usuario de la base de datos
|
||||||
|
# Evita usar caracteres especiales como '@', '#', '?', ':', ';' o espacios
|
||||||
|
DB_PASS="password"
|
||||||
|
|
||||||
|
# Usuario administrador
|
||||||
|
DB_ADMIN="postgres"
|
||||||
|
|
||||||
|
# Contraseña del usuario administrador
|
||||||
|
DB_ADMIN_PASS="adminpassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejecución de los scripts
|
||||||
|
|
||||||
|
Asegúrate de que los scripts tienen permisos de ejecución:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x db-create.sh db-delete.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Y ejecuta el script deseado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./db-create.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
o
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./db-delete.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# 🚧 Advertencia
|
||||||
|
|
||||||
|
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Su API es
|
||||||
|
inestable y está sujeta a cambios frecuentes. No recomiendo su uso en producción, al menos mientras
|
||||||
|
no se libere una versión **1.0.0**.
|
||||||
|
|
||||||
|
|
||||||
|
# 📜 Licencia
|
||||||
|
|
||||||
|
El código está disponible bajo una doble licencia:
|
||||||
|
|
||||||
|
* **Licencia MIT**
|
||||||
|
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
* **Licencia Apache, Versión 2.0**
|
||||||
|
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||||
|
el ecosistema Rust.
|
||||||
6
drust/config/common.toml
Normal file
6
drust/config/common.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[app]
|
||||||
|
name = "Drust"
|
||||||
|
description = "A modern web Content Management System to share your world."
|
||||||
|
|
||||||
|
[database]
|
||||||
|
db_type = "mysql"
|
||||||
7
drust/config/default.toml
Normal file
7
drust/config/default.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[app]
|
||||||
|
#theme = "Aliner"
|
||||||
|
theme = "Bootsier"
|
||||||
|
language = "es-ES"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||||
40
drust/config/predefined.toml
Normal file
40
drust/config/predefined.toml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[app]
|
||||||
|
name = "Sample"
|
||||||
|
description = "Developed with the amazing PageTop framework."
|
||||||
|
# Default theme.
|
||||||
|
theme = "Default"
|
||||||
|
# Default language (localization).
|
||||||
|
language = "en-US"
|
||||||
|
# Default text direction: "ltr", "rtl", or "auto".
|
||||||
|
direction = "ltr"
|
||||||
|
# Startup banner: "Off", "Slant", "Small", "Speed", or "Starwars".
|
||||||
|
startup_banner = "Slant"
|
||||||
|
|
||||||
|
[dev]
|
||||||
|
# Static files required by the app are integrated by default into the executable
|
||||||
|
# binary. However, during development, it can be useful to serve these files
|
||||||
|
# from their own directory to avoid recompiling every time they are modified. In
|
||||||
|
# this case, just indicate the full path to the project's root directory.
|
||||||
|
pagetop_project_dir = ""
|
||||||
|
|
||||||
|
[log]
|
||||||
|
# Execution trace: "Error", "Warn", "Info", "Debug", or "Trace".
|
||||||
|
# For example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
|
||||||
|
tracing = "Info"
|
||||||
|
# In terminal ("Stdout") or files "Daily", "Hourly", "Minutely", or "Endless".
|
||||||
|
rolling = "Stdout"
|
||||||
|
# Directory for trace files (if rolling != "Stdout").
|
||||||
|
path = "log"
|
||||||
|
# Prefix for trace files (if rolling != "Stdout").
|
||||||
|
prefix = "tracing.log"
|
||||||
|
# Traces format: "Full", "Compact", "Pretty", or "Json".
|
||||||
|
format = "Full"
|
||||||
|
|
||||||
|
[server]
|
||||||
|
# Web server config.
|
||||||
|
bind_address = "localhost"
|
||||||
|
bind_port = 8088
|
||||||
|
# Session cookie duration (in seconds), i.e., the time from when the session is
|
||||||
|
# created until the cookie expires. A value of 0 indicates "until the browser is
|
||||||
|
# closed". By default, it is one week.
|
||||||
|
session_lifetime = 604800
|
||||||
39
drust/src/main.rs
Normal file
39
drust/src/main.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
//! <div align="center">
|
||||||
|
//!
|
||||||
|
//! <h1>Drust</h1>
|
||||||
|
//!
|
||||||
|
//! <p>Un Sistema de Gestión de Contenidos (CMS) basado en <strong>PageTop</strong> para compartir tu mundo.</p>
|
||||||
|
//!
|
||||||
|
//! [](#-license)
|
||||||
|
//! [](https://crates.io/crates/drust)
|
||||||
|
//! [](https://crates.io/crates/drust)
|
||||||
|
//!
|
||||||
|
//! </div>
|
||||||
|
//!
|
||||||
|
//! ## Sobre PageTop
|
||||||
|
//!
|
||||||
|
//! [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la
|
||||||
|
//! web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles
|
||||||
|
//! y configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
struct Drust;
|
||||||
|
|
||||||
|
impl ExtensionTrait for Drust {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![
|
||||||
|
// Extensiones.
|
||||||
|
//&pagetop_admin::Admin,
|
||||||
|
//&pagetop_user::User,
|
||||||
|
//&pagetop_node::Node,
|
||||||
|
// Temas.
|
||||||
|
&pagetop_bootsier::Bootsier,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&Drust).run()?.await
|
||||||
|
}
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use pagetop_bootsier::prelude::*;
|
|
||||||
|
|
||||||
struct SuperMenu;
|
|
||||||
|
|
||||||
impl Extension for SuperMenu {
|
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
|
||||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&self) {
|
|
||||||
let home_path = |cx: &Context| match cx.langid().language.as_str() {
|
|
||||||
"en" => "/en",
|
|
||||||
_ => "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new().with_path(Some(home_path)))
|
|
||||||
.with_expand(BreakPoint::LG)
|
|
||||||
.add_item(navbar::Item::nav(
|
|
||||||
Nav::new()
|
|
||||||
.add_item(nav::Item::link(
|
|
||||||
L10n::l("sample_menus_item_link"),
|
|
||||||
home_path,
|
|
||||||
))
|
|
||||||
.add_item(nav::Item::link_blank(
|
|
||||||
L10n::l("sample_menus_item_blank"),
|
|
||||||
|_| "https://docs.rs/pagetop",
|
|
||||||
))
|
|
||||||
.add_item(nav::Item::dropdown(
|
|
||||||
Dropdown::new()
|
|
||||||
.with_title(L10n::l("sample_menus_test_title"))
|
|
||||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_dev_getting_started"),
|
|
||||||
|_| "/dev/getting-started",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_dev_guides"),
|
|
||||||
|_| "/dev/guides",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::link_blank(
|
|
||||||
L10n::l("sample_menus_dev_forum"),
|
|
||||||
|_| "https://forum.example.dev",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::divider())
|
|
||||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_sdk_rust"),
|
|
||||||
|_| "/dev/sdks/rust",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
|
|
||||||
"/dev/sdks/js"
|
|
||||||
}))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_sdk_python"),
|
|
||||||
|_| "/dev/sdks/python",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::divider())
|
|
||||||
.add_item(dropdown::Item::header(L10n::l(
|
|
||||||
"sample_menus_plugin_header",
|
|
||||||
)))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_plugin_auth"),
|
|
||||||
|_| "/dev/sdks/rust/plugins/auth",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::link(
|
|
||||||
L10n::l("sample_menus_plugin_cache"),
|
|
||||||
|_| "/dev/sdks/rust/plugins/cache",
|
|
||||||
))
|
|
||||||
.add_item(dropdown::Item::divider())
|
|
||||||
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
|
|
||||||
.add_item(dropdown::Item::link_disabled(
|
|
||||||
L10n::l("sample_menus_item_disabled"),
|
|
||||||
|_| "#",
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.add_item(nav::Item::link_disabled(
|
|
||||||
L10n::l("sample_menus_item_disabled"),
|
|
||||||
|_| "#",
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.add_item(navbar::Item::nav(
|
|
||||||
Nav::new()
|
|
||||||
.with_classes(
|
|
||||||
ClassesOp::Add,
|
|
||||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
|
||||||
)
|
|
||||||
.add_item(nav::Item::link(
|
|
||||||
L10n::l("sample_menus_item_sign_up"),
|
|
||||||
|_| "/auth/sign-up",
|
|
||||||
))
|
|
||||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
|
|
||||||
"/auth/login"
|
|
||||||
})),
|
|
||||||
));
|
|
||||||
|
|
||||||
InRegion::Named("header").add(Child::with(
|
|
||||||
Container::new()
|
|
||||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
|
||||||
.add_child(navbar_menu),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&SuperMenu).run()?.await
|
|
||||||
}
|
|
||||||
1
extensions/.gitignore
vendored
Normal file
1
extensions/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pagetop-aliner/**
|
||||||
|
|
@ -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,115 +0,0 @@
|
||||||
/*!
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<h1>PageTop Aliner</h1>
|
|
||||||
|
|
||||||
<p>Tema para <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
|
|
||||||
|
|
||||||
[](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()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
|
|
||||||
///
|
|
||||||
/// Define un tema mínimo útil para:
|
|
||||||
///
|
|
||||||
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
|
|
||||||
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
|
|
||||||
/// - Realizar pruebas de renderizado rápido con salida estable y predecible.
|
|
||||||
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
|
||||||
pub struct Aliner;
|
|
||||||
|
|
||||||
impl Extension for Aliner {
|
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
|
||||||
Some(&Self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
|
||||||
static_files_service!(scfg, [aliner] => "/aliner");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme for Aliner {
|
|
||||||
fn after_render_page_body(&self, page: &mut Page) {
|
|
||||||
page.alter_param("include_basic_assets", true)
|
|
||||||
.alter_assets(ContextOp::AddStyleSheet(
|
|
||||||
StyleSheet::from("/aliner/css/styles.css")
|
|
||||||
.with_version(env!("CARGO_PKG_VERSION"))
|
|
||||||
.with_weight(-90),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,356 +0,0 @@
|
||||||
html {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1px 3px;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
padding: 1px 3px;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
padding: 1px 3px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4,h5, h6, p {
|
|
||||||
background-color: snow;
|
|
||||||
}
|
|
||||||
* * {
|
|
||||||
outline: 5px solid rgba(255,0,0,.1);
|
|
||||||
}
|
|
||||||
* * * {
|
|
||||||
outline: 3px dashed rgba(255,0,0,.4);
|
|
||||||
}
|
|
||||||
* * * * {
|
|
||||||
outline: 2px dotted rgba(255,0,0,.6);
|
|
||||||
}
|
|
||||||
* * * * * {
|
|
||||||
outline: 1px dotted rgba(255,0,0,.9);
|
|
||||||
}
|
|
||||||
* * * * * * {
|
|
||||||
outline-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
*::before, *::after {
|
|
||||||
background: #faa;
|
|
||||||
border-radius: 3px;
|
|
||||||
font: normal normal 400 10px/1.2 monospace;
|
|
||||||
vertical-align: middle;
|
|
||||||
padding: 1px 3px;
|
|
||||||
margin: 0 3px;
|
|
||||||
}
|
|
||||||
*::before {
|
|
||||||
content: "(";
|
|
||||||
}
|
|
||||||
*::after {
|
|
||||||
content: ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
a::before { content: "<a>"; }
|
|
||||||
a::after { content: "</a>"; }
|
|
||||||
abbr::before { content: "<abbr>"; }
|
|
||||||
abbr::after { content: "</abbr>"; }
|
|
||||||
acronym::before { content: "<acronym>"; }
|
|
||||||
acronym::after { content: "</acronym>"; }
|
|
||||||
address::before { content: "<address>"; }
|
|
||||||
address::after { content: "</address>"; }
|
|
||||||
applet::before { content: "<applet>"; }
|
|
||||||
applet::after { content: "</applet>"; }
|
|
||||||
area::before { content: "<area>"; }
|
|
||||||
area::after { content: "</area>"; }
|
|
||||||
article::before { content: "<article>"; }
|
|
||||||
article::after { content: "</article>"; }
|
|
||||||
aside::before { content: "<aside>"; }
|
|
||||||
aside::after { content: "</aside>"; }
|
|
||||||
audio::before { content: "<audio>"; }
|
|
||||||
audio::after { content: "</audio>"; }
|
|
||||||
|
|
||||||
b::before { content: "<b>"; }
|
|
||||||
b::after { content: "</b>"; }
|
|
||||||
base::before { content: "<base>"; }
|
|
||||||
base::after { content: "</base>"; }
|
|
||||||
basefont::before { content: "<basefont>"; }
|
|
||||||
basefont::after { content: "</basefont>"; }
|
|
||||||
bdi::before { content: "<bdi>"; }
|
|
||||||
bdi::after { content: "</bdi>"; }
|
|
||||||
bdo::before { content: "<bdo>"; }
|
|
||||||
bdo::after { content: "</bdo>"; }
|
|
||||||
bgsound::before { content: "<bgsound>"; }
|
|
||||||
bgsound::after { content: "</bgsound>"; }
|
|
||||||
big::before { content: "<big>"; }
|
|
||||||
big::after { content: "</big>"; }
|
|
||||||
blink::before { content: "<blink>"; }
|
|
||||||
blink::after { content: "</blink>"; }
|
|
||||||
blockquote::before { content: "<blockquote>"; }
|
|
||||||
blockquote::after { content: "</blockquote>"; }
|
|
||||||
body::before { content: "<body>"; }
|
|
||||||
body::after { content: "</body>"; }
|
|
||||||
br::before { content: "<br>"; }
|
|
||||||
br::after { content: "</br>"; }
|
|
||||||
button::before { content: "<button>"; }
|
|
||||||
button::after { content: "</button>"; }
|
|
||||||
|
|
||||||
caption::before { content: "<caption>"; }
|
|
||||||
caption::after { content: "</caption>"; }
|
|
||||||
canvas::before { content: "<canvas>"; }
|
|
||||||
canvas::after { content: "</canvas>"; }
|
|
||||||
center::before { content: "<center>"; }
|
|
||||||
center::after { content: "</center>"; }
|
|
||||||
cite::before { content: "<cite>"; }
|
|
||||||
cite::after { content: "</cite>"; }
|
|
||||||
code::before { content: "<code>"; }
|
|
||||||
code::after { content: "</code>"; }
|
|
||||||
col::before { content: "<col>"; }
|
|
||||||
col::after { content: "</col>"; }
|
|
||||||
colgroup::before { content: "<colgroup>"; }
|
|
||||||
colgroup::after { content: "</colgroup>"; }
|
|
||||||
command::before { content: "<command>"; }
|
|
||||||
command::after { content: "</command>"; }
|
|
||||||
content::before { content: "<content>"; }
|
|
||||||
content::after { content: "</content>"; }
|
|
||||||
|
|
||||||
data::before { content: "<data>"; }
|
|
||||||
data::after { content: "</data>"; }
|
|
||||||
datalist::before { content: "<datalist>"; }
|
|
||||||
datalist::after { content: "</datalist>"; }
|
|
||||||
dd::before { content: "<dd>"; }
|
|
||||||
dd::after { content: "</dd>"; }
|
|
||||||
del::before { content: "<del>"; }
|
|
||||||
del::after { content: "</del>"; }
|
|
||||||
details::before { content: "<details>"; }
|
|
||||||
details::after { content: "</details>"; }
|
|
||||||
dfn::before { content: "<dfn>"; }
|
|
||||||
dfn::after { content: "</dfn>"; }
|
|
||||||
dialog::before { content: "<dialog>"; }
|
|
||||||
dialog::after { content: "</dialog>"; }
|
|
||||||
dir::before { content: "<dir>"; }
|
|
||||||
dir::after { content: "</dir>"; }
|
|
||||||
div::before { content: "<div>"; }
|
|
||||||
div::after { content: "</div>"; }
|
|
||||||
dl::before { content: "<dl>"; }
|
|
||||||
dl::after { content: "</dl>"; }
|
|
||||||
dt::before { content: "<dt>"; }
|
|
||||||
dt::after { content: "</dt>"; }
|
|
||||||
|
|
||||||
element::before { content: "<element>"; }
|
|
||||||
element::after { content: "</element>"; }
|
|
||||||
em::before { content: "<em>"; }
|
|
||||||
em::after { content: "</em>"; }
|
|
||||||
embed::before { content: "<embed>"; }
|
|
||||||
embed::after { content: "</embed>"; }
|
|
||||||
|
|
||||||
fieldset::before { content: "<fieldset>"; }
|
|
||||||
fieldset::after { content: "</fieldset>"; }
|
|
||||||
figcaption::before { content: "<figcaption>"; }
|
|
||||||
figcaption::after { content: "</figcaption>"; }
|
|
||||||
figure::before { content: "<figure>"; }
|
|
||||||
figure::after { content: "</figure>"; }
|
|
||||||
font::before { content: "<font>"; }
|
|
||||||
font::after { content: "</font>"; }
|
|
||||||
footer::before { content: "<footer>"; }
|
|
||||||
footer::after { content: "</footer>"; }
|
|
||||||
form::before { content: "<form>"; }
|
|
||||||
form::after { content: "</form>"; }
|
|
||||||
frame::before { content: "<frame>"; }
|
|
||||||
frame::after { content: "</frame>"; }
|
|
||||||
frameset::before { content: "<frameset>"; }
|
|
||||||
frameset::after { content: "</frameset>"; }
|
|
||||||
|
|
||||||
h1::before { content: "<h1>"; }
|
|
||||||
h1::after { content: "</h1>"; }
|
|
||||||
h2::before { content: "<h2>"; }
|
|
||||||
h2::after { content: "</h2>"; }
|
|
||||||
h3::before { content: "<h3>"; }
|
|
||||||
h3::after { content: "</h3>"; }
|
|
||||||
h4::before { content: "<h4>"; }
|
|
||||||
h4::after { content: "</h4>"; }
|
|
||||||
h5::before { content: "<h5>"; }
|
|
||||||
h5::after { content: "</h5>"; }
|
|
||||||
h6::before { content: "<h6>"; }
|
|
||||||
h6::after { content: "</h6>"; }
|
|
||||||
head::before { content: "<head>"; }
|
|
||||||
head::after { content: "</head>"; }
|
|
||||||
header::before { content: "<header>"; }
|
|
||||||
header::after { content: "</header>"; }
|
|
||||||
hgroup::before { content: "<hgroup>"; }
|
|
||||||
hgroup::after { content: "</hgroup>"; }
|
|
||||||
hr::before { content: "<hr>"; }
|
|
||||||
hr::after { content: "</hr>"; }
|
|
||||||
html::before { content: "<html>"; }
|
|
||||||
html::after { content: "</html>"; }
|
|
||||||
|
|
||||||
i::before { content: "<i>"; }
|
|
||||||
i::after { content: "</i>"; }
|
|
||||||
iframe::before { content: "<iframe>"; }
|
|
||||||
iframe::after { content: "</iframe>"; }
|
|
||||||
image::before { content: "<image>"; }
|
|
||||||
image::after { content: "</image>"; }
|
|
||||||
img::before { content: "<img>"; }
|
|
||||||
img::after { content: "</img>"; }
|
|
||||||
input::before { content: "<input>"; }
|
|
||||||
input::after { content: "</input>"; }
|
|
||||||
ins::before { content: "<ins>"; }
|
|
||||||
ins::after { content: "</ins>"; }
|
|
||||||
isindex::before { content: "<isindex>"; }
|
|
||||||
isindex::after { content: "</isindex>"; }
|
|
||||||
|
|
||||||
kbd::before { content: "<kbd>"; }
|
|
||||||
kbd::after { content: "</kbd>"; }
|
|
||||||
keygen::before { content: "<keygen>"; }
|
|
||||||
keygen::after { content: "</keygen>"; }
|
|
||||||
|
|
||||||
label::before { content: "<label>"; }
|
|
||||||
label::after { content: "</label>"; }
|
|
||||||
legend::before { content: "<legend>"; }
|
|
||||||
legend::after { content: "</legend>"; }
|
|
||||||
li::before { content: "<li>"; }
|
|
||||||
li::after { content: "</li>"; }
|
|
||||||
link::before { content: "<link>"; }
|
|
||||||
link::after { content: "</link>"; }
|
|
||||||
listing::before { content: "<listing>"; }
|
|
||||||
listing::after { content: "</listing>"; }
|
|
||||||
|
|
||||||
main::before { content: "<main>"; }
|
|
||||||
main::after { content: "</main>"; }
|
|
||||||
map::before { content: "<map>"; }
|
|
||||||
map::after { content: "</map>"; }
|
|
||||||
mark::before { content: "<mark>"; }
|
|
||||||
mark::after { content: "</mark>"; }
|
|
||||||
marquee::before { content: "<marquee>"; }
|
|
||||||
marquee::after { content: "</marquee>"; }
|
|
||||||
menu::before { content: "<menu>"; }
|
|
||||||
menu::after { content: "</menu>"; }
|
|
||||||
menuitem::before { content: "<menuitem>"; }
|
|
||||||
menuitem::after { content: "</menuitem>"; }
|
|
||||||
meta::before { content: "<meta>"; }
|
|
||||||
meta::after { content: "</meta>"; }
|
|
||||||
meter::before { content: "<meter>"; }
|
|
||||||
meter::after { content: "</meter>"; }
|
|
||||||
multicol::before { content: "<multicol>"; }
|
|
||||||
multicol::after { content: "</multicol>"; }
|
|
||||||
|
|
||||||
nav::before { content: "<nav>"; }
|
|
||||||
nav::after { content: "</nav>"; }
|
|
||||||
nextid::before { content: "<nextid>"; }
|
|
||||||
nextid::after { content: "</nextid>"; }
|
|
||||||
nobr::before { content: "<nobr>"; }
|
|
||||||
nobr::after { content: "</nobr>"; }
|
|
||||||
noembed::before { content: "<noembed>"; }
|
|
||||||
noembed::after { content: "</noembed>"; }
|
|
||||||
noframes::before { content: "<noframes>"; }
|
|
||||||
noframes::after { content: "</noframes>"; }
|
|
||||||
noscript::before { content: "<noscript>"; }
|
|
||||||
noscript::after { content: "</noscript>"; }
|
|
||||||
|
|
||||||
object::before { content: "<object>"; }
|
|
||||||
object::after { content: "</object>"; }
|
|
||||||
ol::before { content: "<ol>"; }
|
|
||||||
ol::after { content: "</ol>"; }
|
|
||||||
optgroup::before { content: "<optgroup>"; }
|
|
||||||
optgroup::after { content: "</optgroup>"; }
|
|
||||||
option::before { content: "<option>"; }
|
|
||||||
option::after { content: "</option>"; }
|
|
||||||
output::before { content: "<output>"; }
|
|
||||||
output::after { content: "</output>"; }
|
|
||||||
|
|
||||||
p::before { content: "<p>"; }
|
|
||||||
p::after { content: "</p>"; }
|
|
||||||
param::before { content: "<param>"; }
|
|
||||||
param::after { content: "</param>"; }
|
|
||||||
picture::before { content: "<picture>"; }
|
|
||||||
picture::after { content: "</picture>"; }
|
|
||||||
plaintext::before { content: "<plaintext>"; }
|
|
||||||
plaintext::after { content: "</plaintext>"; }
|
|
||||||
pre::before { content: "<pre>"; }
|
|
||||||
pre::after { content: "</pre>"; }
|
|
||||||
progress::before { content: "<progress>"; }
|
|
||||||
progress::after { content: "</progress>"; }
|
|
||||||
|
|
||||||
q::before { content: "<q>"; }
|
|
||||||
q::after { content: "</q>"; }
|
|
||||||
|
|
||||||
rb::before { content: "<rb>"; }
|
|
||||||
rb::after { content: "</rb>"; }
|
|
||||||
rp::before { content: "<rp>"; }
|
|
||||||
rp::after { content: "</rp>"; }
|
|
||||||
rt::before { content: "<rt>"; }
|
|
||||||
rt::after { content: "</rt>"; }
|
|
||||||
rtc::before { content: "<rtc>"; }
|
|
||||||
rtc::after { content: "</rtc>"; }
|
|
||||||
ruby::before { content: "<ruby>"; }
|
|
||||||
ruby::after { content: "</ruby>"; }
|
|
||||||
|
|
||||||
s::before { content: "<s>"; }
|
|
||||||
s::after { content: "</s>"; }
|
|
||||||
samp::before { content: "<samp>"; }
|
|
||||||
samp::after { content: "</samp>"; }
|
|
||||||
script::before { content: "<script>"; }
|
|
||||||
script::after { content: "</script>"; }
|
|
||||||
section::before { content: "<section>"; }
|
|
||||||
section::after { content: "</section>"; }
|
|
||||||
select::before { content: "<select>"; }
|
|
||||||
select::after { content: "</select>"; }
|
|
||||||
shadow::before { content: "<shadow>"; }
|
|
||||||
shadow::after { content: "</shadow>"; }
|
|
||||||
slot::before { content: "<slot>"; }
|
|
||||||
slot::after { content: "</slot>"; }
|
|
||||||
small::before { content: "<small>"; }
|
|
||||||
small::after { content: "</small>"; }
|
|
||||||
source::before { content: "<source>"; }
|
|
||||||
source::after { content: "</source>"; }
|
|
||||||
spacer::before { content: "<spacer>"; }
|
|
||||||
spacer::after { content: "</spacer>"; }
|
|
||||||
span::before { content: "<span>"; }
|
|
||||||
span::after { content: "</span>"; }
|
|
||||||
strike::before { content: "<strike>"; }
|
|
||||||
strike::after { content: "</strike>"; }
|
|
||||||
strong::before { content: "<strong>"; }
|
|
||||||
strong::after { content: "</strong>"; }
|
|
||||||
style::before { content: "<style>"; }
|
|
||||||
style::after { content: "<\/style>"; }
|
|
||||||
sub::before { content: "<sub>"; }
|
|
||||||
sub::after { content: "</sub>"; }
|
|
||||||
summary::before { content: "<summary>"; }
|
|
||||||
summary::after { content: "</summary>"; }
|
|
||||||
sup::before { content: "<sup>"; }
|
|
||||||
sup::after { content: "</sup>"; }
|
|
||||||
|
|
||||||
table::before { content: "<table>"; }
|
|
||||||
table::after { content: "</table>"; }
|
|
||||||
tbody::before { content: "<tbody>"; }
|
|
||||||
tbody::after { content: "</tbody>"; }
|
|
||||||
td::before { content: "<td>"; }
|
|
||||||
td::after { content: "</td>"; }
|
|
||||||
template::before { content: "<template>"; }
|
|
||||||
template::after { content: "</template>"; }
|
|
||||||
textarea::before { content: "<textarea>"; }
|
|
||||||
textarea::after { content: "</textarea>"; }
|
|
||||||
tfoot::before { content: "<tfoot>"; }
|
|
||||||
tfoot::after { content: "</tfoot>"; }
|
|
||||||
th::before { content: "<th>"; }
|
|
||||||
th::after { content: "</th>"; }
|
|
||||||
thead::before { content: "<thead>"; }
|
|
||||||
thead::after { content: "</thead>"; }
|
|
||||||
time::before { content: "<time>"; }
|
|
||||||
time::after { content: "</time>"; }
|
|
||||||
title::before { content: "<title>"; }
|
|
||||||
title::after { content: "</title>"; }
|
|
||||||
tr::before { content: "<tr>"; }
|
|
||||||
tr::after { content: "</tr>"; }
|
|
||||||
track::before { content: "<track>"; }
|
|
||||||
track::after { content: "</track>"; }
|
|
||||||
tt::before { content: "<tt>"; }
|
|
||||||
tt::after { content: "</tt>"; }
|
|
||||||
|
|
||||||
u::before { content: "<u>"; }
|
|
||||||
u::after { content: "</u>"; }
|
|
||||||
ul::before { content: "<ul>"; }
|
|
||||||
ul::after { content: "</ul>"; }
|
|
||||||
|
|
||||||
var::before { content: "<var>"; }
|
|
||||||
var::after { content: "</var>"; }
|
|
||||||
video::before { content: "<video>"; }
|
|
||||||
video::after { content: "</video>"; }
|
|
||||||
|
|
||||||
wbr::before { content: "<wbr>"; }
|
|
||||||
wbr::after { content: "</wbr>"; }
|
|
||||||
|
|
||||||
xmp::before { content: "<xmp>"; }
|
|
||||||
xmp::after { content: "</xmp>"; }
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pagetop-bootsier"
|
name = "pagetop-bootsier"
|
||||||
version = "0.0.18"
|
version = "0.0.20"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
description = """
|
description = """\
|
||||||
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
Tema para PageTop que usa Bootstrap para dar vida a tus diseños web.\
|
||||||
"""
|
"""
|
||||||
categories = ["web-programming", "gui"]
|
categories = ["web-programming", "gui"]
|
||||||
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
||||||
|
|
||||||
repository.workspace = true
|
homepage = { workspace = true }
|
||||||
homepage.workspace = true
|
repository = { workspace = true }
|
||||||
license.workspace = true
|
authors = { workspace = true }
|
||||||
authors.workspace = true
|
license = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pagetop.workspace = true
|
pagetop.workspace = true
|
||||||
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
static-files.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pagetop-build.workspace = true
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2022 Manuel Cillero
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022 Manuel Cillero
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
@ -2,14 +2,13 @@
|
||||||
|
|
||||||
<h1>PageTop Bootsier</h1>
|
<h1>PageTop Bootsier</h1>
|
||||||
|
|
||||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
<p>Tema para <strong>PageTop</strong> que usa Bootstrap para dar vida a tus diseños web.</p>
|
||||||
|
|
||||||
|
[](#-license)
|
||||||
[](https://docs.rs/pagetop-bootsier)
|
[](https://docs.rs/pagetop-bootsier)
|
||||||
[](https://crates.io/crates/pagetop-bootsier)
|
[](https://crates.io/crates/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>
|
</div>
|
||||||
|
|
||||||
## Sobre PageTop
|
## Sobre PageTop
|
||||||
|
|
@ -19,72 +18,11 @@ clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares
|
||||||
configurables, basadas en HTML, CSS y JavaScript.
|
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
|
# 🚧 Advertencia
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
`PageTop` es un proyecto personal que hago por diversión para aprender cosas nuevas. Está en
|
||||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
desarrollo activo, su API es inestable y está sujeta a cambios frecuentes. No recomiendo su uso en
|
||||||
hasta que se libere la versión **1.0.0**.
|
producción, al menos hasta liberar la versión **1.0.0**.
|
||||||
|
|
||||||
|
|
||||||
# 📜 Licencia
|
# 📜 Licencia
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bootstrap_js_files(path: &Path) -> bool {
|
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".
|
// No filtra durante el desarrollo, solo en la compilación "release".
|
||||||
env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release"
|
env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release"
|
||||||
|| path.file_name().is_some_and(|f| f == bootstrap_js)
|
|| path.file_name().map_or(false, |n| n == "bootstrap.min.js")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
143
extensions/pagetop-bootsier/src/bs.rs
Normal file
143
extensions/pagetop-bootsier/src/bs.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
// Utilities.
|
||||||
|
mod utility;
|
||||||
|
pub use utility::*;
|
||||||
|
|
||||||
|
// Container.
|
||||||
|
pub mod container;
|
||||||
|
pub use container::{Container, ContainerType};
|
||||||
|
|
||||||
|
// Grid.
|
||||||
|
pub mod grid;
|
||||||
|
pub use grid::Grid;
|
||||||
|
|
||||||
|
// Offcanvas.
|
||||||
|
pub mod offcanvas;
|
||||||
|
pub use offcanvas::{
|
||||||
|
Offcanvas, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Image.
|
||||||
|
mod image;
|
||||||
|
pub use image::{Image, ImageSize};
|
||||||
|
|
||||||
|
// Navbar.
|
||||||
|
pub mod navbar;
|
||||||
|
pub use navbar::{Navbar, NavbarContent, NavbarToggler};
|
||||||
|
|
||||||
|
// Dropdown.
|
||||||
|
pub mod dropdown;
|
||||||
|
pub use dropdown::Dropdown;
|
||||||
|
|
||||||
|
/// Define los puntos de interrupción (*breakpoints*) usados por Bootstrap para diseño responsivo.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BreakPoint {
|
||||||
|
#[default] // DIMENSIONES - DISPOSITIVOS ---------------------------------------------------
|
||||||
|
None, // < 576px Muy pequeños: teléfonos en modo vertical, menos de 576px
|
||||||
|
SM, // >= 576px Pequeños: teléfonos en modo horizontal, 576px o más
|
||||||
|
MD, // >= 768px Medianos: tabletas, 768px o más
|
||||||
|
LG, // >= 992px Grandes: puestos de escritorio, 992px o más
|
||||||
|
XL, // >= 1200px Muy grandes: puestos de escritorio grandes, 1200px o más
|
||||||
|
XXL, // >= 1400px Extragrandes: puestos de escritorio más grandes, 1400px o más
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
Fluid, // Para Container, aplica el 100% del dispositivo siempre
|
||||||
|
FluidMax(unit::Value) // Para Container, aplica el 100% del dispositivo hasta un ancho máximo
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BreakPoint {
|
||||||
|
/// Verifica si es un punto de interrupción efectivo en Bootstrap.
|
||||||
|
///
|
||||||
|
/// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`. Y `false` en otro caso.
|
||||||
|
pub fn is_breakpoint(&self) -> bool {
|
||||||
|
!matches!(
|
||||||
|
self,
|
||||||
|
BreakPoint::None | BreakPoint::Fluid | BreakPoint::FluidMax(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Genera un nombre de clase CSS basado en el punto de interrupción.
|
||||||
|
///
|
||||||
|
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
|
||||||
|
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso
|
||||||
|
/// devuelve únicamente el prefijo.
|
||||||
|
///
|
||||||
|
/// # Parámetros
|
||||||
|
///
|
||||||
|
/// - `prefix`: Prefijo para concatenar con el punto de interrupción.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust#ignore
|
||||||
|
/// let breakpoint = BreakPoint::MD;
|
||||||
|
/// let class = breakpoint.to_class("col");
|
||||||
|
/// assert_eq!(class, "col-md".to_string());
|
||||||
|
///
|
||||||
|
/// let breakpoint = BreakPoint::Fluid;
|
||||||
|
/// let class = breakpoint.to_class("offcanvas");
|
||||||
|
/// assert_eq!(class, "offcanvas".to_string());
|
||||||
|
/// ```
|
||||||
|
pub fn to_class(&self, prefix: impl Into<String>) -> String {
|
||||||
|
let prefix: String = prefix.into();
|
||||||
|
if self.is_breakpoint() {
|
||||||
|
join_string!(prefix, "-", self.to_string())
|
||||||
|
} else {
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intenta generar un nombre de clase CSS basado en el punto de interrupción.
|
||||||
|
///
|
||||||
|
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
|
||||||
|
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso,
|
||||||
|
/// devuelve `None`.
|
||||||
|
///
|
||||||
|
/// # Parámetros
|
||||||
|
///
|
||||||
|
/// - `prefix`: Prefijo a concatenar con el punto de interrupción.
|
||||||
|
///
|
||||||
|
/// # Retorno
|
||||||
|
///
|
||||||
|
/// - `Some(String)`: Si es un punto de interrupción efectivo.
|
||||||
|
/// - `None`: En otro caso.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust#ignore
|
||||||
|
/// let breakpoint = BreakPoint::MD;
|
||||||
|
/// let class = breakpoint.try_class("col");
|
||||||
|
/// assert_eq!(class, Some("col-md".to_string()));
|
||||||
|
///
|
||||||
|
/// let breakpoint = BreakPoint::Fluid;
|
||||||
|
/// let class = breakpoint.try_class("navbar-expanded");
|
||||||
|
/// assert_eq!(class, None);
|
||||||
|
/// ```
|
||||||
|
pub fn try_class(&self, prefix: impl Into<String>) -> Option<String> {
|
||||||
|
let prefix: String = prefix.into();
|
||||||
|
if self.is_breakpoint() {
|
||||||
|
Some(join_string!(prefix, "-", self.to_string()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BreakPoint {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BreakPoint::None => write!(f, ""),
|
||||||
|
BreakPoint::SM => write!(f, "sm"),
|
||||||
|
BreakPoint::MD => write!(f, "md"),
|
||||||
|
BreakPoint::LG => write!(f, "lg"),
|
||||||
|
BreakPoint::XL => write!(f, "xl"),
|
||||||
|
BreakPoint::XXL => write!(f, "xxl"),
|
||||||
|
BreakPoint::Fluid => write!(f, "fluid"),
|
||||||
|
BreakPoint::FluidMax(_) => write!(f, "fluid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
172
extensions/pagetop-bootsier/src/bs/container.rs
Normal file
172
extensions/pagetop-bootsier/src/bs/container.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::BreakPoint;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ContainerType {
|
||||||
|
#[default]
|
||||||
|
Default, // Contenedor genérico
|
||||||
|
Main, // Contenido principal
|
||||||
|
Header, // Encabezado
|
||||||
|
Footer, // Pie
|
||||||
|
Section, // Sección específica de contenido
|
||||||
|
Article, // Artículo dentro de una sección
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Container {
|
||||||
|
id : OptionId,
|
||||||
|
classes : OptionClasses,
|
||||||
|
container_type: ContainerType,
|
||||||
|
breakpoint : BreakPoint,
|
||||||
|
children : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Container {
|
||||||
|
fn new() -> Self {
|
||||||
|
Container::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
trio_string!("container", "-", self.breakpoint().to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let output = self.children().render(cx);
|
||||||
|
if output.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
let style = if let BreakPoint::FluidMax(max_width) = self.breakpoint() {
|
||||||
|
Some(join_string!("max-width: ", max_width.to_string(), ";"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match self.container_type() {
|
||||||
|
ContainerType::Default => PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ContainerType::Main => PrepareMarkup::With(html! {
|
||||||
|
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ContainerType::Header => PrepareMarkup::With(html! {
|
||||||
|
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ContainerType::Footer => PrepareMarkup::With(html! {
|
||||||
|
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ContainerType::Section => PrepareMarkup::With(html! {
|
||||||
|
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ContainerType::Article => PrepareMarkup::With(html! {
|
||||||
|
article id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
pub fn main() -> Self {
|
||||||
|
Container {
|
||||||
|
container_type: ContainerType::Main,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header() -> Self {
|
||||||
|
Container {
|
||||||
|
container_type: ContainerType::Header,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn footer() -> Self {
|
||||||
|
Container {
|
||||||
|
container_type: ContainerType::Footer,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section() -> Self {
|
||||||
|
Container {
|
||||||
|
container_type: ContainerType::Section,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn article() -> Self {
|
||||||
|
Container {
|
||||||
|
container_type: ContainerType::Article,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
|
||||||
|
self.breakpoint = bp;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
|
||||||
|
self.children.add(Child::with(child));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_children(mut self, op: ChildOp) -> Self {
|
||||||
|
self.children.alter_child(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn container_type(&self) -> &ContainerType {
|
||||||
|
&self.container_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn breakpoint(&self) -> &BreakPoint {
|
||||||
|
&self.breakpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self) -> &Children {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
5
extensions/pagetop-bootsier/src/bs/dropdown.rs
Normal file
5
extensions/pagetop-bootsier/src/bs/dropdown.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod component;
|
||||||
|
pub use component::Dropdown;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::Item;
|
||||||
99
extensions/pagetop-bootsier/src/bs/dropdown/component.rs
Normal file
99
extensions/pagetop-bootsier/src/bs/dropdown/component.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::dropdown;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Dropdown {
|
||||||
|
id : OptionId,
|
||||||
|
classes: OptionClasses,
|
||||||
|
items : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Dropdown {
|
||||||
|
fn new() -> Self {
|
||||||
|
Dropdown::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "dropdown");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let items = self.items().render(cx);
|
||||||
|
if items.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
|
button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
{
|
||||||
|
("Dropdown button")
|
||||||
|
}
|
||||||
|
ul class="dropdown-menu" {
|
||||||
|
li {
|
||||||
|
a class="dropdown-item" href="#" {
|
||||||
|
("Action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
a class="dropdown-item" href="#" {
|
||||||
|
("Another action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
a class="dropdown-item" href="#" {
|
||||||
|
("Something else here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dropdown {
|
||||||
|
// Dropdown BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_item(mut self, item: dropdown::Item) -> Self {
|
||||||
|
self.items.add(Child::with(item));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
109
extensions/pagetop-bootsier/src/bs/dropdown/item.rs
Normal file
109
extensions/pagetop-bootsier/src/bs/dropdown/item.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
type Label = L10n;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemType {
|
||||||
|
#[default]
|
||||||
|
Void,
|
||||||
|
Label(Label),
|
||||||
|
Link(Label, FnContextualPath),
|
||||||
|
LinkBlank(Label, FnContextualPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item.
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Item {
|
||||||
|
item_type: ItemType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Item {
|
||||||
|
fn new() -> Self {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let description: Option<String> = None;
|
||||||
|
|
||||||
|
// Obtiene la URL actual desde `cx.request`.
|
||||||
|
let current_path = cx.request().path();
|
||||||
|
|
||||||
|
match self.item_type() {
|
||||||
|
ItemType::Void => PrepareMarkup::None,
|
||||||
|
ItemType::Label(label) => PrepareMarkup::With(html! {
|
||||||
|
li class="dropdown-item" {
|
||||||
|
span title=[description] {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemType::Link(label, path) => {
|
||||||
|
let item_path = path(cx);
|
||||||
|
let (class, aria) = if item_path == current_path {
|
||||||
|
("dropdown-item active", Some("page"))
|
||||||
|
} else {
|
||||||
|
("dropdown-item", None)
|
||||||
|
};
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li class=(class) aria-current=[aria] {
|
||||||
|
a class="nav-link" href=(item_path) title=[description] {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ItemType::LinkBlank(label, path) => {
|
||||||
|
let item_path = path(cx);
|
||||||
|
let (class, aria) = if item_path == current_path {
|
||||||
|
("dropdown-item active", Some("page"))
|
||||||
|
} else {
|
||||||
|
("dropdown-item", None)
|
||||||
|
};
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li class=(class) aria-current=[aria] {
|
||||||
|
a class="nav-link" href=(item_path) title=[description] target="_blank" {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn label(label: L10n) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::Label(label),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(label: L10n, path: FnContextualPath) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::Link(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link_blank(label: L10n, path: FnContextualPath) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::LinkBlank(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item GETTERS.
|
||||||
|
|
||||||
|
pub fn item_type(&self) -> &ItemType {
|
||||||
|
&self.item_type
|
||||||
|
}
|
||||||
|
}
|
||||||
110
extensions/pagetop-bootsier/src/bs/grid.rs
Normal file
110
extensions/pagetop-bootsier/src/bs/grid.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::BreakPoint;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::Grid;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::Item;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Layout {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Rows(u8),
|
||||||
|
Cols(u8),
|
||||||
|
Grid(u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for Layout {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Layout::Rows(r) if *r > 1 => write!(f, "--bs-rows: {r};"),
|
||||||
|
Layout::Cols(c) if *c > 0 => write!(f, "--bs-columns: {c};"),
|
||||||
|
Layout::Grid(r, c) => write!(f, "{}", trio_string!(
|
||||||
|
Layout::Rows(*r).to_string(), " ", Layout::Cols(*c).to_string()
|
||||||
|
)),
|
||||||
|
_ => write!(f, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Gap {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Row(unit::Value),
|
||||||
|
Col(unit::Value),
|
||||||
|
Grid(unit::Value, unit::Value),
|
||||||
|
Both(unit::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for Gap {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Gap::Default => write!(f, ""),
|
||||||
|
Gap::Row(r) => write!(f, "row-gap: {r};"),
|
||||||
|
Gap::Col(c) => write!(f, "column-gap: {c};"),
|
||||||
|
Gap::Grid(r, c) => write!(f, "--bs-gap: {r} {c};"),
|
||||||
|
Gap::Both(v) => write!(f, "--bs-gap: {v};"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemColumns {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Cols(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for ItemColumns {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ItemColumns::Cols(c) if *c > 1 => write!(f, "g-col-{c}"),
|
||||||
|
_ => write!(f, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemResponsive {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Cols(BreakPoint, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for ItemResponsive {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ItemResponsive::Cols(bp, c) if bp.is_breakpoint() && *c > 0 => {
|
||||||
|
write!(f, "g-col-{bp}-{c}")
|
||||||
|
}
|
||||||
|
_ => write!(f, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemStart {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Col(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for ItemStart {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ItemStart::Col(c) if *c > 1 => write!(f, "g-start-{c}"),
|
||||||
|
_ => write!(f, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
extensions/pagetop-bootsier/src/bs/grid/component.rs
Normal file
103
extensions/pagetop-bootsier/src/bs/grid/component.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::grid;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Grid {
|
||||||
|
id : OptionId,
|
||||||
|
classes : OptionClasses,
|
||||||
|
grid_layout: grid::Layout,
|
||||||
|
grid_gap : grid::Gap,
|
||||||
|
items : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Grid {
|
||||||
|
fn new() -> Self {
|
||||||
|
Grid::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "grid");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let output = self.items().render(cx);
|
||||||
|
if output.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let style = option_string!([self.layout().to_string(), self.gap().to_string()]; " ");
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
pub fn with(item: grid::Item) -> Self {
|
||||||
|
Grid::default().with_item(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_layout(mut self, layout: grid::Layout) -> Self {
|
||||||
|
self.grid_layout = layout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_gap(mut self, gap: grid::Gap) -> Self {
|
||||||
|
self.grid_gap = gap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_item(mut self, item: grid::Item) -> Self {
|
||||||
|
self.items.add(Child::with(item));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<grid::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout(&self) -> &grid::Layout {
|
||||||
|
&self.grid_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gap(&self) -> &grid::Gap {
|
||||||
|
&self.grid_gap
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
119
extensions/pagetop-bootsier/src/bs/grid/item.rs
Normal file
119
extensions/pagetop-bootsier/src/bs/grid/item.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::grid;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Item {
|
||||||
|
id : OptionId,
|
||||||
|
classes : OptionClasses,
|
||||||
|
columns : grid::ItemColumns,
|
||||||
|
responsive: grid::ItemResponsive,
|
||||||
|
start : grid::ItemStart,
|
||||||
|
children : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Item {
|
||||||
|
fn new() -> Self {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
[
|
||||||
|
self.columns().to_string(),
|
||||||
|
self.responsive().to_string(),
|
||||||
|
self.start().to_string(),
|
||||||
|
]
|
||||||
|
.join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let output = self.children().render(cx);
|
||||||
|
if output.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
|
(output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn with(child: impl ComponentTrait) -> Self {
|
||||||
|
Item::default().with_child(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_columns(mut self, columns: grid::ItemColumns) -> Self {
|
||||||
|
self.columns = columns;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_responsive(mut self, responsive: grid::ItemResponsive) -> Self {
|
||||||
|
self.responsive = responsive;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_start(mut self, start: grid::ItemStart) -> Self {
|
||||||
|
self.start = start;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
|
||||||
|
self.children.add(Child::with(child));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_children(mut self, op: ChildOp) -> Self {
|
||||||
|
self.children.alter_child(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn columns(&self) -> &grid::ItemColumns {
|
||||||
|
&self.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn responsive(&self) -> &grid::ItemResponsive {
|
||||||
|
&self.responsive
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) -> &grid::ItemStart {
|
||||||
|
&self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self) -> &Children {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
157
extensions/pagetop-bootsier/src/bs/image.rs
Normal file
157
extensions/pagetop-bootsier/src/bs/image.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::Border;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ImageType {
|
||||||
|
#[default]
|
||||||
|
Fluid,
|
||||||
|
Thumbnail,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for ImageType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ImageType::Fluid => write!(f, "img-fluid"),
|
||||||
|
ImageType::Thumbnail => write!(f, "img-thumbnail"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ImageSize {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Size(u16, u16),
|
||||||
|
Width(u16),
|
||||||
|
Height(u16),
|
||||||
|
Both(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Image {
|
||||||
|
id : OptionId,
|
||||||
|
classes : OptionClasses,
|
||||||
|
image_type: ImageType,
|
||||||
|
source : OptionString,
|
||||||
|
size : ImageSize,
|
||||||
|
border : Border,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Image {
|
||||||
|
fn new() -> Self {
|
||||||
|
Image::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
[self.image_type().to_string()].join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let (width, height) = match self.size() {
|
||||||
|
ImageSize::Auto => (None, None),
|
||||||
|
ImageSize::Size(width, height) => (Some(width), Some(height)),
|
||||||
|
ImageSize::Width(width) => (Some(width), None),
|
||||||
|
ImageSize::Height(height) => (None, Some(height)),
|
||||||
|
ImageSize::Both(value) => (Some(value), Some(value)),
|
||||||
|
};
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
img
|
||||||
|
src=[self.source().get()]
|
||||||
|
id=[self.id()]
|
||||||
|
class=[self.classes().get()]
|
||||||
|
width=[width]
|
||||||
|
height=[height] {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn with(source: &str) -> Self {
|
||||||
|
Image::default().with_source(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail(source: &str) -> Self {
|
||||||
|
Image::default()
|
||||||
|
.with_source(source)
|
||||||
|
.with_image_type(ImageType::Thumbnail)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub fn pagetop() -> Self {
|
||||||
|
Image::default()
|
||||||
|
.with_source("/base/pagetop-logo.svg")
|
||||||
|
.with_classes(ClassesOp::Add, IMG_FIXED)
|
||||||
|
.with_size(ImageSize::Size(64, 64))
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Image BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_image_type(mut self, image_type: ImageType) -> Self {
|
||||||
|
self.image_type = image_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_source(mut self, source: &str) -> Self {
|
||||||
|
self.source.alter_value(source);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_size(mut self, size: ImageSize) -> Self {
|
||||||
|
self.size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_border(mut self, border: Border) -> Self {
|
||||||
|
self.border = border;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn image_type(&self) -> &ImageType {
|
||||||
|
&self.image_type
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &OptionString {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> &ImageSize {
|
||||||
|
&self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border(&self) -> &Border {
|
||||||
|
&self.border
|
||||||
|
}
|
||||||
|
}
|
||||||
11
extensions/pagetop-bootsier/src/bs/navbar.rs
Normal file
11
extensions/pagetop-bootsier/src/bs/navbar.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
mod component;
|
||||||
|
pub use component::{Navbar, NavbarContent, NavbarToggler};
|
||||||
|
|
||||||
|
//mod brand;
|
||||||
|
//pub use brand::Brand;
|
||||||
|
|
||||||
|
mod nav;
|
||||||
|
pub use nav::Nav;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::{Item, ItemType};
|
||||||
102
extensions/pagetop-bootsier/src/bs/navbar/brand.rs
Normal file
102
extensions/pagetop-bootsier/src/bs/navbar/brand.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Brand {
|
||||||
|
id : OptionId,
|
||||||
|
#[default(_code = "global::SETTINGS.app.name.to_owned()")]
|
||||||
|
app_name : String,
|
||||||
|
slogan : OptionTranslated,
|
||||||
|
logo : OptionComponent<Image>,
|
||||||
|
#[default(_code = "|_| \"/\"")]
|
||||||
|
home : FnContextualPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Brand {
|
||||||
|
fn new() -> Self {
|
||||||
|
Brand::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let logo = self.logo().render(cx);
|
||||||
|
let home = self.home()(cx);
|
||||||
|
let title = &L10n::l("site_home").using(cx.langid());
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div id=[self.id()] class="branding__container" {
|
||||||
|
div class="branding__content" {
|
||||||
|
@if !logo.is_empty() {
|
||||||
|
a class="branding__logo" href=(home) title=[title] rel="home" {
|
||||||
|
(logo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="branding__text" {
|
||||||
|
a class="branding__name" href=(home) title=[title] rel="home" {
|
||||||
|
(self.app_name())
|
||||||
|
}
|
||||||
|
@if let Some(slogan) = self.slogan().using(cx.langid()) {
|
||||||
|
div class="branding__slogan" {
|
||||||
|
(slogan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Brand {
|
||||||
|
// Brand BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
|
||||||
|
self.app_name = app_name.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_slogan(mut self, slogan: L10n) -> Self {
|
||||||
|
self.slogan.alter_value(slogan);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_logo(mut self, logo: Option<Image>) -> Self {
|
||||||
|
self.logo.alter_value(logo);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_home(mut self, home: FnContextualPath) -> Self {
|
||||||
|
self.home = home;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brand GETTERS.
|
||||||
|
|
||||||
|
pub fn app_name(&self) -> &String {
|
||||||
|
&self.app_name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slogan(&self) -> &OptionTranslated {
|
||||||
|
&self.slogan
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logo(&self) -> &OptionComponent<Image> {
|
||||||
|
&self.logo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn home(&self) -> &FnContextualPath {
|
||||||
|
&self.home
|
||||||
|
}
|
||||||
|
}
|
||||||
202
extensions/pagetop-bootsier/src/bs/navbar/component.rs
Normal file
202
extensions/pagetop-bootsier/src/bs/navbar/component.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::navbar;
|
||||||
|
use crate::bs::{BreakPoint, Offcanvas};
|
||||||
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
|
||||||
|
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||||
|
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum NavbarToggler {
|
||||||
|
#[default]
|
||||||
|
Enabled,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum NavbarContent {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Nav(Typed<navbar::Nav>),
|
||||||
|
Offcanvas(Typed<Offcanvas>),
|
||||||
|
Text(L10n),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Navbar {
|
||||||
|
id : OptionId,
|
||||||
|
classes: OptionClasses,
|
||||||
|
expand : BreakPoint,
|
||||||
|
toggler: NavbarToggler,
|
||||||
|
content: NavbarContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Navbar {
|
||||||
|
fn new() -> Self {
|
||||||
|
Navbar::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
[
|
||||||
|
"navbar".to_string(),
|
||||||
|
self.expand().try_class("navbar-expand").unwrap_or_default(),
|
||||||
|
]
|
||||||
|
.join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let id = cx.required_id::<Self>(self.id());
|
||||||
|
|
||||||
|
let content = match self.content() {
|
||||||
|
NavbarContent::None => return PrepareMarkup::None,
|
||||||
|
NavbarContent::Nav(nav) => {
|
||||||
|
let id_content = join_string!(id, "-content");
|
||||||
|
match self.toggler() {
|
||||||
|
NavbarToggler::Enabled => self.toggler_wrapper(
|
||||||
|
TOGGLE_COLLAPSE,
|
||||||
|
L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
|
||||||
|
id_content,
|
||||||
|
nav.render(cx),
|
||||||
|
),
|
||||||
|
NavbarToggler::Disabled => nav.render(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavbarContent::Offcanvas(oc) => {
|
||||||
|
let id_content = oc.id().unwrap_or_default();
|
||||||
|
self.toggler_wrapper(
|
||||||
|
TOGGLE_OFFCANVAS,
|
||||||
|
L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
|
||||||
|
id_content,
|
||||||
|
oc.render(cx),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NavbarContent::Text(text) => html! {
|
||||||
|
span class="navbar-text" {
|
||||||
|
(text.escaped(cx.langid()))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.nav_wrapper(id, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Navbar {
|
||||||
|
pub fn with_nav(nav: navbar::Nav) -> Self {
|
||||||
|
Navbar::default().with_content(NavbarContent::Nav(Typed::with(nav)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
|
||||||
|
Navbar::default().with_content(NavbarContent::Offcanvas(Typed::with(offcanvas)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
|
||||||
|
self.expand = bp;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
|
||||||
|
self.toggler = toggler;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_content(mut self, content: NavbarContent) -> Self {
|
||||||
|
self.content = content;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand(&self) -> &BreakPoint {
|
||||||
|
&self.expand
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggler(&self) -> &NavbarToggler {
|
||||||
|
&self.toggler
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> &NavbarContent {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar HELPERS.
|
||||||
|
|
||||||
|
fn nav_wrapper(&self, id: String, content: Markup) -> PrepareMarkup {
|
||||||
|
if content.is_empty() {
|
||||||
|
PrepareMarkup::None
|
||||||
|
} else {
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
nav id=(id) class=[self.classes().get()] {
|
||||||
|
div class="container-fluid" {
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggler_wrapper(
|
||||||
|
&self,
|
||||||
|
data_bs_toggle: &str,
|
||||||
|
aria_label: Option<String>,
|
||||||
|
id_content: String,
|
||||||
|
content: Markup,
|
||||||
|
) -> Markup {
|
||||||
|
if content.is_empty() {
|
||||||
|
html! {}
|
||||||
|
} else {
|
||||||
|
let id_content_target = join_string!("#", id_content);
|
||||||
|
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
||||||
|
Some("false")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
button
|
||||||
|
type="button"
|
||||||
|
class="navbar-toggler"
|
||||||
|
data-bs-toggle=(data_bs_toggle)
|
||||||
|
data-bs-target=(id_content_target)
|
||||||
|
aria-controls=(id_content)
|
||||||
|
aria-expanded=[aria_expanded]
|
||||||
|
aria-label=[aria_label]
|
||||||
|
{
|
||||||
|
span class="navbar-toggler-icon" {}
|
||||||
|
}
|
||||||
|
div id=(id_content) class="collapse navbar-collapse" {
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
extensions/pagetop-bootsier/src/bs/navbar/item.rs
Normal file
113
extensions/pagetop-bootsier/src/bs/navbar/item.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::Dropdown;
|
||||||
|
|
||||||
|
type Label = L10n;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemType {
|
||||||
|
#[default]
|
||||||
|
Void,
|
||||||
|
Label(Label),
|
||||||
|
Link(Label, FnContextualPath),
|
||||||
|
LinkBlank(Label, FnContextualPath),
|
||||||
|
Dropdown(Typed<Dropdown>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item.
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Item {
|
||||||
|
item_type: ItemType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Item {
|
||||||
|
fn new() -> Self {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let description: Option<String> = None;
|
||||||
|
|
||||||
|
// Obtiene la URL actual desde `cx.request`.
|
||||||
|
let current_path = cx.request().path();
|
||||||
|
|
||||||
|
match self.item_type() {
|
||||||
|
ItemType::Void => PrepareMarkup::None,
|
||||||
|
ItemType::Label(label) => PrepareMarkup::With(html! {
|
||||||
|
li class="nav-item" {
|
||||||
|
span title=[description] {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ItemType::Link(label, path) => {
|
||||||
|
let item_path = path(cx);
|
||||||
|
let (class, aria) = if item_path == current_path {
|
||||||
|
("nav-item active", Some("page"))
|
||||||
|
} else {
|
||||||
|
("nav-item", None)
|
||||||
|
};
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li class=(class) aria-current=[aria] {
|
||||||
|
a class="nav-link" href=(item_path) title=[description] {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ItemType::LinkBlank(label, path) => {
|
||||||
|
let item_path = path(cx);
|
||||||
|
let (class, aria) = if item_path == current_path {
|
||||||
|
("nav-item active", Some("page"))
|
||||||
|
} else {
|
||||||
|
("nav-item", None)
|
||||||
|
};
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li class=(class) aria-current=[aria] {
|
||||||
|
a class="nav-link" href=(item_path) title=[description] target="_blank" {
|
||||||
|
//(left_icon)
|
||||||
|
(label.escaped(cx.langid()))
|
||||||
|
//(right_icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ItemType::Dropdown(menu) => PrepareMarkup::With(html! { (menu.render(cx)) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn label(label: L10n) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::Label(label),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(label: L10n, path: FnContextualPath) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::Link(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link_blank(label: L10n, path: FnContextualPath) -> Self {
|
||||||
|
Item {
|
||||||
|
item_type: ItemType::LinkBlank(label, path),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item GETTERS.
|
||||||
|
|
||||||
|
pub fn item_type(&self) -> &ItemType {
|
||||||
|
&self.item_type
|
||||||
|
}
|
||||||
|
}
|
||||||
75
extensions/pagetop-bootsier/src/bs/navbar/nav.rs
Normal file
75
extensions/pagetop-bootsier/src/bs/navbar/nav.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::navbar;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Nav {
|
||||||
|
id : OptionId,
|
||||||
|
classes: OptionClasses,
|
||||||
|
items : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Nav {
|
||||||
|
fn new() -> Self {
|
||||||
|
Nav::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let items = self.items().render(cx);
|
||||||
|
if items.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
ul id=[self.id()] class=[self.classes().get()] {
|
||||||
|
(items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nav {
|
||||||
|
// Nav BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_item(mut self, item: navbar::Item) -> Self {
|
||||||
|
self.items.add(Child::with(item));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nav GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
59
extensions/pagetop-bootsier/src/bs/offcanvas.rs
Normal file
59
extensions/pagetop-bootsier/src/bs/offcanvas.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::Offcanvas;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum OffcanvasPlacement {
|
||||||
|
#[default]
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for OffcanvasPlacement {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
OffcanvasPlacement::Start => write!(f, "offcanvas-start"),
|
||||||
|
OffcanvasPlacement::End => write!(f, "offcanvas-end"),
|
||||||
|
OffcanvasPlacement::Top => write!(f, "offcanvas-top"),
|
||||||
|
OffcanvasPlacement::Bottom => write!(f, "offcanvas-bottom"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum OffcanvasVisibility {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Show,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for OffcanvasVisibility {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
OffcanvasVisibility::Default => write!(f, "show"),
|
||||||
|
OffcanvasVisibility::Show => write!(f, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum OffcanvasBodyScroll {
|
||||||
|
#[default]
|
||||||
|
Disabled,
|
||||||
|
Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum OffcanvasBackdrop {
|
||||||
|
Disabled,
|
||||||
|
#[default]
|
||||||
|
Enabled,
|
||||||
|
Static,
|
||||||
|
}
|
||||||
187
extensions/pagetop-bootsier/src/bs/offcanvas/component.rs
Normal file
187
extensions/pagetop-bootsier/src/bs/offcanvas/component.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::BreakPoint;
|
||||||
|
use crate::bs::{OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility};
|
||||||
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Offcanvas {
|
||||||
|
id : OptionId,
|
||||||
|
classes : OptionClasses,
|
||||||
|
title : OptionTranslated,
|
||||||
|
breakpoint: BreakPoint,
|
||||||
|
placement : OffcanvasPlacement,
|
||||||
|
visibility: OffcanvasVisibility,
|
||||||
|
scrolling : OffcanvasBodyScroll,
|
||||||
|
backdrop : OffcanvasBackdrop,
|
||||||
|
children : Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentTrait for Offcanvas {
|
||||||
|
fn new() -> Self {
|
||||||
|
Offcanvas::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
[
|
||||||
|
self.breakpoint().to_class("offcanvas"),
|
||||||
|
self.placement().to_string(),
|
||||||
|
self.visibility().to_string(),
|
||||||
|
]
|
||||||
|
.join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let body = self.children().render(cx);
|
||||||
|
if body.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = cx.required_id::<Self>(self.id());
|
||||||
|
let id_label = join_string!(id, "-label");
|
||||||
|
let id_target = join_string!("#", id);
|
||||||
|
|
||||||
|
let body_scroll = match self.body_scroll() {
|
||||||
|
OffcanvasBodyScroll::Disabled => None,
|
||||||
|
OffcanvasBodyScroll::Enabled => Some("true".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let backdrop = match self.backdrop() {
|
||||||
|
OffcanvasBackdrop::Disabled => Some("true".to_string()),
|
||||||
|
OffcanvasBackdrop::Enabled => None,
|
||||||
|
OffcanvasBackdrop::Static => Some("static".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
div
|
||||||
|
id=(id)
|
||||||
|
class=[self.classes().get()]
|
||||||
|
tabindex="-1"
|
||||||
|
data-bs-scroll=[body_scroll]
|
||||||
|
data-bs-backdrop=[backdrop]
|
||||||
|
aria-labelledby=(id_label)
|
||||||
|
{
|
||||||
|
div class="offcanvas-header" {
|
||||||
|
h5 class="offcanvas-title" id=(id_label) {
|
||||||
|
(self.title().escaped(cx.langid()))
|
||||||
|
}
|
||||||
|
button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="offcanvas"
|
||||||
|
data-bs-target=(id_target)
|
||||||
|
aria-label=[L10n::t("close", &LOCALES_BOOTSIER).using(cx.langid())]
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
div class="offcanvas-body" {
|
||||||
|
(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Offcanvas {
|
||||||
|
// Offcanvas BUILDER.
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_title(mut self, title: L10n) -> Self {
|
||||||
|
self.title.alter_value(title);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
|
||||||
|
self.breakpoint = bp;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_placement(mut self, placement: OffcanvasPlacement) -> Self {
|
||||||
|
self.placement = placement;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_visibility(mut self, visibility: OffcanvasVisibility) -> Self {
|
||||||
|
self.visibility = visibility;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_body_scroll(mut self, scrolling: OffcanvasBodyScroll) -> Self {
|
||||||
|
self.scrolling = scrolling;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_backdrop(mut self, backdrop: OffcanvasBackdrop) -> Self {
|
||||||
|
self.backdrop = backdrop;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_child(mut self, child: impl ComponentTrait) -> Self {
|
||||||
|
self.children.add(Child::with(child));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[fn_builder]
|
||||||
|
pub fn with_children(mut self, op: ChildOp) -> Self {
|
||||||
|
self.children.alter_child(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offcanvas GETTERS.
|
||||||
|
|
||||||
|
pub fn classes(&self) -> &OptionClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &OptionTranslated {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn breakpoint(&self) -> &BreakPoint {
|
||||||
|
&self.breakpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placement(&self) -> &OffcanvasPlacement {
|
||||||
|
&self.placement
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn visibility(&self) -> &OffcanvasVisibility {
|
||||||
|
&self.visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn body_scroll(&self) -> &OffcanvasBodyScroll {
|
||||||
|
&self.scrolling
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backdrop(&self) -> &OffcanvasBackdrop {
|
||||||
|
&self.backdrop
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn children(&self) -> &Children {
|
||||||
|
&self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
10
extensions/pagetop-bootsier/src/bs/utility.rs
Normal file
10
extensions/pagetop-bootsier/src/bs/utility.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
mod color;
|
||||||
|
pub use color::Color;
|
||||||
|
pub use color::{BgColor, BorderColor, TextColor};
|
||||||
|
|
||||||
|
mod opacity;
|
||||||
|
pub use opacity::Opacity;
|
||||||
|
pub use opacity::{BgOpacity, BorderOpacity, TextOpacity};
|
||||||
|
|
||||||
|
mod border;
|
||||||
|
pub use border::{Border, BorderSize};
|
||||||
102
extensions/pagetop-bootsier/src/bs/utility/border.rs
Normal file
102
extensions/pagetop-bootsier/src/bs/utility/border.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use pagetop::{prelude::*, strict_string};
|
||||||
|
|
||||||
|
use crate::bs::{BorderColor, BorderOpacity};
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BorderSize {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Zero,
|
||||||
|
Width1,
|
||||||
|
Width2,
|
||||||
|
Width3,
|
||||||
|
Width4,
|
||||||
|
Width5,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BorderSize {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BorderSize::Default => write!(f, ""),
|
||||||
|
BorderSize::Zero => write!(f, "0"),
|
||||||
|
BorderSize::Width1 => write!(f, "1"),
|
||||||
|
BorderSize::Width2 => write!(f, "2"),
|
||||||
|
BorderSize::Width3 => write!(f, "3"),
|
||||||
|
BorderSize::Width4 => write!(f, "4"),
|
||||||
|
BorderSize::Width5 => write!(f, "5"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Border {
|
||||||
|
color : BorderColor,
|
||||||
|
opacity: BorderOpacity,
|
||||||
|
size : BorderSize,
|
||||||
|
top : BorderSize,
|
||||||
|
end : BorderSize,
|
||||||
|
bottom : BorderSize,
|
||||||
|
start : BorderSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Border {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with(size: BorderSize) -> Self {
|
||||||
|
Self::default().with_size(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border BUILDER.
|
||||||
|
|
||||||
|
pub fn with_color(mut self, color: BorderColor) -> Self {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_opacity(mut self, opacity: BorderOpacity) -> Self {
|
||||||
|
self.opacity = opacity;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_size(mut self, size: BorderSize) -> Self {
|
||||||
|
self.size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_top(mut self, size: BorderSize) -> Self {
|
||||||
|
self.top = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_end(mut self, size: BorderSize) -> Self {
|
||||||
|
self.end = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_bottom(mut self, size: BorderSize) -> Self {
|
||||||
|
self.bottom = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_start(mut self, size: BorderSize) -> Self {
|
||||||
|
self.start = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for Border {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", strict_string!([
|
||||||
|
"border",
|
||||||
|
&self.color.to_string(),
|
||||||
|
&self.opacity.to_string(),
|
||||||
|
]; " ").unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
119
extensions/pagetop-bootsier/src/bs/utility/color.rs
Normal file
119
extensions/pagetop-bootsier/src/bs/utility/color.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Color {
|
||||||
|
#[default]
|
||||||
|
Primary,
|
||||||
|
Secondary,
|
||||||
|
Success,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Danger,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for Color {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Color::Primary => write!(f, "primary"),
|
||||||
|
Color::Secondary => write!(f, "secondary"),
|
||||||
|
Color::Success => write!(f, "success"),
|
||||||
|
Color::Info => write!(f, "info"),
|
||||||
|
Color::Warning => write!(f, "warning"),
|
||||||
|
Color::Danger => write!(f, "danger"),
|
||||||
|
Color::Light => write!(f, "light"),
|
||||||
|
Color::Dark => write!(f, "dark"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BgColor {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Body,
|
||||||
|
BodySecondary,
|
||||||
|
BodyTertiary,
|
||||||
|
Theme(Color),
|
||||||
|
Subtle(Color),
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
Transparent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BgColor {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BgColor::Default => write!(f, ""),
|
||||||
|
BgColor::Body => write!(f, "bg-body"),
|
||||||
|
BgColor::BodySecondary => write!(f, "bg-body-secondary"),
|
||||||
|
BgColor::BodyTertiary => write!(f, "bg-body-tertiary"),
|
||||||
|
BgColor::Theme(c) => write!(f, "bg-{}", c),
|
||||||
|
BgColor::Subtle(c) => write!(f, "bg-{}-subtle", c),
|
||||||
|
BgColor::Black => write!(f, "bg-black"),
|
||||||
|
BgColor::White => write!(f, "bg-white"),
|
||||||
|
BgColor::Transparent => write!(f, "bg-transparent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BorderColor {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Theme(Color),
|
||||||
|
Subtle(Color),
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BorderColor {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BorderColor::Default => write!(f, ""),
|
||||||
|
BorderColor::Theme(c) => write!(f, "border-{}", c),
|
||||||
|
BorderColor::Subtle(c) => write!(f, "border-{}-subtle", c),
|
||||||
|
BorderColor::Black => write!(f, "border-black"),
|
||||||
|
BorderColor::White => write!(f, "border-white"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum TextColor {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Body,
|
||||||
|
BodyEmphasis,
|
||||||
|
BodySecondary,
|
||||||
|
BodyTertiary,
|
||||||
|
Theme(Color),
|
||||||
|
Emphasis(Color),
|
||||||
|
Background(Color),
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for TextColor {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
TextColor::Default => write!(f, ""),
|
||||||
|
TextColor::Body => write!(f, "text-body"),
|
||||||
|
TextColor::BodyEmphasis => write!(f, "text-body-emphasis"),
|
||||||
|
TextColor::BodySecondary => write!(f, "text-body-secondary"),
|
||||||
|
TextColor::BodyTertiary => write!(f, "text-body-tertiary"),
|
||||||
|
TextColor::Theme(c) => write!(f, "text-{}", c),
|
||||||
|
TextColor::Emphasis(c) => write!(f, "text-{}-emphasis", c),
|
||||||
|
TextColor::Background(c) => write!(f, "text-bg-{}", c),
|
||||||
|
TextColor::Black => write!(f, "text-black"),
|
||||||
|
TextColor::White => write!(f, "text-white"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
extensions/pagetop-bootsier/src/bs/utility/opacity.rs
Normal file
78
extensions/pagetop-bootsier/src/bs/utility/opacity.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Opacity {
|
||||||
|
#[default]
|
||||||
|
Opaque, // 100%
|
||||||
|
SemiOpaque, // 75%
|
||||||
|
Half, // 50%
|
||||||
|
SemiTransparent, // 25%
|
||||||
|
AlmostTransparent, // 10%
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for Opacity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Opacity::Opaque => write!(f, "opacity-100"),
|
||||||
|
Opacity::SemiOpaque => write!(f, "opacity-75"),
|
||||||
|
Opacity::Half => write!(f, "opacity-50"),
|
||||||
|
Opacity::SemiTransparent => write!(f, "opacity-25"),
|
||||||
|
Opacity::AlmostTransparent => write!(f, "opacity-10"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BgOpacity {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Theme(Opacity),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BgOpacity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BgOpacity::Default => write!(f, ""),
|
||||||
|
BgOpacity::Theme(o) => write!(f, "bg-{}", o),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BorderOpacity {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Theme(Opacity),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for BorderOpacity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
BorderOpacity::Default => write!(f, ""),
|
||||||
|
BorderOpacity::Theme(o) => write!(f, "border-{}", o),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum TextOpacity {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Theme(Opacity),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for TextOpacity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
TextOpacity::Default => write!(f, ""),
|
||||||
|
TextOpacity::Theme(o) => write!(f, "text-{}", o),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
extensions/pagetop-bootsier/src/bs/utility/rounded.rs
Normal file
116
extensions/pagetop-bootsier/src/bs/utility/rounded.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::bs::{Color, Opacity};
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BorderSize {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Width1,
|
||||||
|
Width2,
|
||||||
|
Width3,
|
||||||
|
Width4,
|
||||||
|
Width5,
|
||||||
|
Free(unit::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum BorderRadius {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Rounded1,
|
||||||
|
Rounded2,
|
||||||
|
Rounded3,
|
||||||
|
Rounded4,
|
||||||
|
Rounded5,
|
||||||
|
Circle,
|
||||||
|
Pill,
|
||||||
|
Free(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct BorderProperty {
|
||||||
|
color : Color,
|
||||||
|
opacity: Opacity,
|
||||||
|
size : BorderSize,
|
||||||
|
radius : BorderRadius,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorderProperty {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
BorderProperty::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_color(mut self, color: Color) -> Self {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
||||||
|
self.opacity = opacity;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_size(mut self, size: BorderSize) -> Self {
|
||||||
|
self.size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_radius(mut self, radius: BorderRadius) -> Self {
|
||||||
|
self.radius = radius;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Border {
|
||||||
|
all : Option<BorderProperty>,
|
||||||
|
top : Option<BorderProperty>,
|
||||||
|
end : Option<BorderProperty>,
|
||||||
|
bottom: Option<BorderProperty>,
|
||||||
|
start : Option<BorderProperty>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Border {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border BUILDER.
|
||||||
|
|
||||||
|
pub fn with_all(mut self, border: BorderProperty) -> Self {
|
||||||
|
self.all = Some(border);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_top(mut self, border: BorderProperty) -> Self {
|
||||||
|
self.top = Some(border);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_end(mut self, border: BorderProperty) -> Self {
|
||||||
|
self.end = Some(border);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_bottom(mut self, border: BorderProperty) -> Self {
|
||||||
|
self.bottom = Some(border);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_start(mut self, border: BorderProperty) -> Self {
|
||||||
|
self.start = Some(border);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_none(mut self) -> Self {
|
||||||
|
self.all = None;
|
||||||
|
self.top = None;
|
||||||
|
self.end = None;
|
||||||
|
self.bottom = None;
|
||||||
|
self.start = None;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Opciones de configuración del tema.
|
//! Opciones de configuración.
|
||||||
//!
|
//!
|
||||||
//! Ejemplo:
|
//! Ejemplo:
|
||||||
//!
|
//!
|
||||||
|
|
@ -9,15 +9,14 @@
|
||||||
//!
|
//!
|
||||||
//! Uso:
|
//! Uso:
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust#ignore
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! use pagetop_bootsier::config;
|
//! use pagetop_bootsier::config;
|
||||||
//!
|
//!
|
||||||
//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440));
|
//! assert_eq!(config::SETTINGS.bootsier.max_width, unit::Value::Rem(90));
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
|
//! Consulta [`pagetop::config`] para aprender cómo `PageTop` lee los archivos de opciones y aplica
|
||||||
//! los valores a los ajustes.
|
//! los valores de configuración.
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
|
@ -29,13 +28,16 @@ include_config!(SETTINGS: Settings => [
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
|
/// Opciones de configuración para la sección [`[bootsier]`](Bootsier) (ver [`SETTINGS`]).
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub bootsier: Bootsier,
|
pub bootsier: Bootsier,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
|
/// Sección `[bootsier]` de la configuración.
|
||||||
|
///
|
||||||
|
/// Ver [`Settings`].
|
||||||
pub struct Bootsier {
|
pub struct Bootsier {
|
||||||
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
||||||
pub max_width: UnitValue,
|
/// Valor por defecto: *"1440px"*
|
||||||
|
pub max_width: unit::Value,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,221 @@
|
||||||
/*!
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<h1>PageTop Bootsier</h1>
|
|
||||||
|
|
||||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
|
||||||
|
|
||||||
[](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::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
// GLOBAL ******************************************************************************************
|
||||||
|
|
||||||
impl Extension for MyApp {
|
include_files!(bootsier_bs);
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
include_files!(bootsier_js);
|
||||||
vec![
|
|
||||||
// ...
|
|
||||||
&pagetop_bootsier::Bootsier,
|
|
||||||
// ...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
Application::prepare(&MyApp).run()?.await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Y **selecciona el tema en la configuración** de la aplicación:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[app]
|
|
||||||
theme = "Bootsier"
|
|
||||||
```
|
|
||||||
|
|
||||||
…o **fuerza el tema por código** en una página concreta:
|
|
||||||
|
|
||||||
```rust,no_run
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
use pagetop_bootsier::Bootsier;
|
|
||||||
|
|
||||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|
||||||
Page::new(request)
|
|
||||||
.with_theme(&Bootsier)
|
|
||||||
.add_child(
|
|
||||||
Block::new()
|
|
||||||
.with_title(L10n::l("sample_title"))
|
|
||||||
.add_child(Html::with(|cx| html! {
|
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.render()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
|
|
||||||
#![doc(
|
|
||||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
|
||||||
)]
|
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
include_locales!(LOCALES_BOOTSIER);
|
include_locales!(LOCALES_BOOTSIER);
|
||||||
|
|
||||||
// Versión de la librería Bootstrap.
|
const BOOTSTRAP_VERSION: &str = "5.3.3"; // Versión de la librería Bootstrap.
|
||||||
const BOOTSTRAP_VERSION: &str = "5.3.8";
|
|
||||||
|
// API *********************************************************************************************
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
pub mod theme;
|
pub mod bs;
|
||||||
|
|
||||||
/// *Prelude* del tema.
|
|
||||||
pub mod prelude {
|
|
||||||
pub use crate::config::*;
|
|
||||||
pub use crate::theme::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Implementa el tema.
|
|
||||||
pub struct Bootsier;
|
pub struct Bootsier;
|
||||||
|
|
||||||
impl Extension for Bootsier {
|
impl ExtensionTrait for Bootsier {
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
Some(&Self)
|
Some(&Bootsier)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actions(&self) -> Vec<ActionBox> {
|
||||||
|
actions![
|
||||||
|
//action::theme::BeforeRender::<Region>::new(&Self, before_render_region),
|
||||||
|
//action::theme::BeforePrepare::<Button>::new(&Self, before_prepare_button),
|
||||||
|
//action::theme::BeforePrepare::<Heading>::new(&Self, before_prepare_heading),
|
||||||
|
//action::theme::BeforePrepare::<Paragraph>::new(&Self, before_prepare_paragraph),
|
||||||
|
//action::theme::RenderComponent::<Error404>::new(&Self, render_error404),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||||
static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
|
include_files_service!(scfg, bootsier_bs => "/bootsier/bs");
|
||||||
static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
|
include_files_service!(scfg, bootsier_js => "/bootsier/js");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme for Bootsier {
|
impl ThemeTrait for Bootsier {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn regions(&self) -> Vec<(&'static str, L10n)> {
|
||||||
|
vec![
|
||||||
|
("region-header", L10n::t("header", &LOCALES_BOOTSIER)),
|
||||||
|
("region-nav_branding", L10n::t("nav_branding", &LOCALES_BOOTSIER)),
|
||||||
|
("region-nav_main", L10n::t("nav_main", &LOCALES_BOOTSIER)),
|
||||||
|
("region-nav_additional", L10n::t("nav_additional", &LOCALES_BOOTSIER)),
|
||||||
|
("region-breadcrumb", L10n::t("breadcrumb", &LOCALES_BOOTSIER)),
|
||||||
|
("region-content", L10n::t("content", &LOCALES_BOOTSIER)),
|
||||||
|
("region-sidebar_first", L10n::t("sidebar_first", &LOCALES_BOOTSIER)),
|
||||||
|
("region-sidebar_second", L10n::t("sidebar_second", &LOCALES_BOOTSIER)),
|
||||||
|
("region-footer", L10n::t("footer", &LOCALES_BOOTSIER)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_page_body(&self, page: &mut Page) -> Markup {
|
||||||
|
html! {
|
||||||
|
body id=[page.body_id().get()] class=[page.body_classes().get()] {
|
||||||
|
//@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
|
||||||
|
// div class="skip__to_content" {
|
||||||
|
// a href=(concat_string!("#", skip_to_id)) { (skip) }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
(bs::Container::new()
|
||||||
|
.with_id("container-wrapper")
|
||||||
|
.with_breakpoint(bs::BreakPoint::FluidMax(config::SETTINGS.bootsier.max_width))
|
||||||
|
.with_child(Region::of("region-content"))
|
||||||
|
.render(page.context()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn after_render_page_body(&self, page: &mut Page) {
|
fn after_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(ContextOp::AddStyleSheet(
|
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-90),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(ContextOp::AddJavaScript(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
JavaScript::defer("/bootsier/js/bootstrap.min.js")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-90),
|
.with_weight(-99),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
|
||||||
|
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
body id=[page.body_id().get()] class=[page.body_classes().get()] {
|
||||||
|
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
|
||||||
|
div class="skip__to_content" {
|
||||||
|
a href=(concat_string!("#", skip_to_id)) { (skip) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(match page.context().layout() {
|
||||||
|
"admin" => flex::Container::new()
|
||||||
|
.add_item(flex::Item::region().with_id("top-menu"))
|
||||||
|
.add_item(flex::Item::region().with_id("side-menu"))
|
||||||
|
.add_item(flex::Item::region().with_id("content")),
|
||||||
|
_ => flex::Container::new()
|
||||||
|
.add_item(flex::Item::region().with_id("header"))
|
||||||
|
.add_item(flex::Item::region().with_id("nav_branding"))
|
||||||
|
.add_item(flex::Item::region().with_id("nav_main"))
|
||||||
|
.add_item(flex::Item::region().with_id("nav_additional"))
|
||||||
|
.add_item(flex::Item::region().with_id("breadcrumb"))
|
||||||
|
.add_item(flex::Item::region().with_id("content"))
|
||||||
|
.add_item(flex::Item::region().with_id("sidebar_first"))
|
||||||
|
.add_item(flex::Item::region().with_id("sidebar_second"))
|
||||||
|
.add_item(flex::Item::region().with_id("footer")),
|
||||||
|
}.render(page.context()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_prepare_icon(i: &mut Icon, _cx: &mut Context) {
|
||||||
|
i.set_classes(
|
||||||
|
ClassesOp::Replace(i.font_size().to_string()),
|
||||||
|
with_font(i.font_size()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn before_prepare_button(b: &mut Button, _cx: &mut Context) {
|
||||||
|
b.set_classes(ClassesOp::Replace("button__tap".to_owned()), "btn");
|
||||||
|
b.set_classes(
|
||||||
|
ClassesOp::Replace(b.style().to_string()),
|
||||||
|
match b.style() {
|
||||||
|
StyleBase::Default => "btn-primary",
|
||||||
|
StyleBase::Info => "btn-info",
|
||||||
|
StyleBase::Success => "btn-success",
|
||||||
|
StyleBase::Warning => "btn-warning",
|
||||||
|
StyleBase::Danger => "btn-danger",
|
||||||
|
StyleBase::Light => "btn-light",
|
||||||
|
StyleBase::Dark => "btn-dark",
|
||||||
|
StyleBase::Link => "btn-link",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
b.set_classes(
|
||||||
|
ClassesOp::Replace(b.font_size().to_string()),
|
||||||
|
with_font(b.font_size()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn before_prepare_heading(h: &mut Heading, _cx: &mut Context) {
|
||||||
|
h.set_classes(
|
||||||
|
ClassesOp::Replace(h.size().to_string()),
|
||||||
|
match h.size() {
|
||||||
|
HeadingSize::ExtraLarge => "display-1",
|
||||||
|
HeadingSize::XxLarge => "display-2",
|
||||||
|
HeadingSize::XLarge => "display-3",
|
||||||
|
HeadingSize::Large => "display-4",
|
||||||
|
HeadingSize::Medium => "display-5",
|
||||||
|
_ => "",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn before_prepare_paragraph(p: &mut Paragraph, _cx: &mut Context) {
|
||||||
|
p.set_classes(
|
||||||
|
ClassesOp::Replace(p.font_size().to_string()),
|
||||||
|
with_font(p.font_size()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_error404(_: &Error404, cx: &mut Context) -> Option<Markup> {
|
||||||
|
Some(html! {
|
||||||
|
div class="jumbotron" {
|
||||||
|
div class="media" {
|
||||||
|
img
|
||||||
|
src="/bootsier/images/caution.png"
|
||||||
|
class="mr-4"
|
||||||
|
style="width: 20%; max-width: 188px"
|
||||||
|
alt="Caution!";
|
||||||
|
div class="media-body" {
|
||||||
|
h1 class="display-4" { ("RESOURCE NOT FOUND") }
|
||||||
|
p class="lead" {
|
||||||
|
(L10n::t("e404-description", &LOCALES_BOOTSIER)
|
||||||
|
.escaped(cx.langid()))
|
||||||
|
}
|
||||||
|
hr class="my-4";
|
||||||
|
p {
|
||||||
|
(L10n::t("e404-description", &LOCALES_BOOTSIER)
|
||||||
|
.escaped(cx.langid()))
|
||||||
|
}
|
||||||
|
a
|
||||||
|
class="btn btn-primary btn-lg"
|
||||||
|
href="/"
|
||||||
|
role="button"
|
||||||
|
{
|
||||||
|
(L10n::t("back-homepage", &LOCALES_BOOTSIER)
|
||||||
|
.escaped(cx.langid()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn with_font(font_size: &FontSize) -> String {
|
||||||
|
String::from(match font_size {
|
||||||
|
FontSize::ExtraLarge => "fs-1",
|
||||||
|
FontSize::XxLarge => "fs-2",
|
||||||
|
FontSize::XLarge => "fs-3",
|
||||||
|
FontSize::Large => "fs-4",
|
||||||
|
FontSize::Medium => "fs-5",
|
||||||
|
_ => "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
# Dropdown
|
|
||||||
dropdown_toggle = Toggle Dropdown
|
|
||||||
|
|
||||||
# Offcanvas
|
# Offcanvas
|
||||||
offcanvas_close = Close
|
close = Close
|
||||||
|
|
||||||
# Navbar
|
# Navbar
|
||||||
toggle = Toggle navigation
|
toggle = Toggle navigation
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
# Dropdown
|
|
||||||
dropdown_toggle = Mostrar/ocultar menú
|
|
||||||
|
|
||||||
# Offcanvas
|
# Offcanvas
|
||||||
offcanvas_close = Cerrar
|
close = Cerrar
|
||||||
|
|
||||||
# Navbar
|
# Navbar
|
||||||
toggle = Mostrar/ocultar navegación
|
toggle = Mostrar/ocultar navegación
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
//! Definiciones y componentes del tema.
|
|
||||||
//!
|
|
||||||
//! En esta página, el apartado **Modules** incluye las definiciones necesarias para los componentes
|
|
||||||
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
|
|
||||||
//! auxiliares del tema utilizados en clases y componentes.
|
|
||||||
|
|
||||||
mod aux;
|
|
||||||
pub use aux::*;
|
|
||||||
|
|
||||||
pub mod classes;
|
|
||||||
|
|
||||||
// Container.
|
|
||||||
pub mod container;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use container::Container;
|
|
||||||
|
|
||||||
// Dropdown.
|
|
||||||
pub mod dropdown;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use dropdown::Dropdown;
|
|
||||||
|
|
||||||
// Image.
|
|
||||||
pub mod image;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use image::Image;
|
|
||||||
|
|
||||||
// Nav.
|
|
||||||
pub mod nav;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use nav::Nav;
|
|
||||||
|
|
||||||
// Navbar.
|
|
||||||
pub mod navbar;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use navbar::Navbar;
|
|
||||||
|
|
||||||
// Offcanvas.
|
|
||||||
pub mod offcanvas;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use offcanvas::Offcanvas;
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
//! Colección de elementos auxiliares de Bootstrap para Bootsier.
|
|
||||||
|
|
||||||
mod breakpoint;
|
|
||||||
pub use breakpoint::BreakPoint;
|
|
||||||
|
|
||||||
mod color;
|
|
||||||
pub use color::{Color, Opacity};
|
|
||||||
pub use color::{ColorBg, ColorText};
|
|
||||||
|
|
||||||
mod layout;
|
|
||||||
pub use layout::{ScaleSize, Side};
|
|
||||||
|
|
||||||
mod border;
|
|
||||||
pub use border::BorderColor;
|
|
||||||
|
|
||||||
mod rounded;
|
|
||||||
pub use rounded::RoundedRadius;
|
|
||||||
|
|
||||||
mod button;
|
|
||||||
pub use button::{ButtonColor, ButtonSize};
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::Color;
|
|
||||||
|
|
||||||
/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum BorderColor {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Genera internamente clases `border-{color}`.
|
|
||||||
Theme(Color),
|
|
||||||
/// Genera internamente clases `border-{color}-subtle` (un tono suavizado del color).
|
|
||||||
Subtle(Color),
|
|
||||||
/// Color negro.
|
|
||||||
Black,
|
|
||||||
/// Color blanco.
|
|
||||||
White,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorderColor {
|
|
||||||
const BORDER: &str = "border";
|
|
||||||
const BORDER_PREFIX: &str = "border-";
|
|
||||||
|
|
||||||
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Default => None,
|
|
||||||
Self::Theme(_) => Some(""),
|
|
||||||
Self::Subtle(_) => Some("-subtle"),
|
|
||||||
Self::Black => Some("-black"),
|
|
||||||
Self::White => Some("-white"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase `border-*` a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => {
|
|
||||||
classes.push_str(Self::BORDER_PREFIX);
|
|
||||||
classes.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => classes.push_str(Self::BORDER),
|
|
||||||
}
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `border-*` correspondiente al color de borde.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
|
|
||||||
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
|
|
||||||
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
|
||||||
/// assert_eq!(BorderColor::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let base_len = match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => Self::BORDER_PREFIX.len() + c.as_str().len(),
|
|
||||||
_ => Self::BORDER.len(),
|
|
||||||
};
|
|
||||||
let mut class = String::with_capacity(base_len + suffix.len());
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => {
|
|
||||||
class.push_str(Self::BORDER_PREFIX);
|
|
||||||
class.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => class.push_str(Self::BORDER),
|
|
||||||
}
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
/// Define los puntos de ruptura (*breakpoints*) para aplicar diseño *responsive*.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum BreakPoint {
|
|
||||||
/// **Menos de 576px**. Dispositivos muy pequeños: teléfonos en modo vertical.
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
/// **576px o más** - Dispositivos pequeños: teléfonos en modo horizontal.
|
|
||||||
SM,
|
|
||||||
/// **768px o más** - Dispositivos medianos: tabletas.
|
|
||||||
MD,
|
|
||||||
/// **992px o más** - Dispositivos grandes: puestos de escritorio.
|
|
||||||
LG,
|
|
||||||
/// **1200px o más** - Dispositivos muy grandes: puestos de escritorio grandes.
|
|
||||||
XL,
|
|
||||||
/// **1400px o más** - Dispositivos extragrandes: puestos de escritorio más grandes.
|
|
||||||
XXL,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BreakPoint {
|
|
||||||
// Devuelve la identificación del punto de ruptura.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::None => "",
|
|
||||||
Self::SM => "sm",
|
|
||||||
Self::MD => "md",
|
|
||||||
Self::LG => "lg",
|
|
||||||
Self::XL => "xl",
|
|
||||||
Self::XXL => "xxl",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
|
|
||||||
// la cadena de clases.
|
|
||||||
//
|
|
||||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
|
||||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str, suffix: &str) {
|
|
||||||
if prefix.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::None => classes.push_str(prefix),
|
|
||||||
_ => {
|
|
||||||
classes.push_str(prefix);
|
|
||||||
classes.push('-');
|
|
||||||
classes.push_str(self.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !suffix.is_empty() {
|
|
||||||
classes.push('-');
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
|
|
||||||
// por un guion `-`.
|
|
||||||
//
|
|
||||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
|
||||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
|
||||||
// - Si `prefix` está vacío devuelve `""`.
|
|
||||||
//
|
|
||||||
// # Ejemplos
|
|
||||||
//
|
|
||||||
// ```rust
|
|
||||||
// # use pagetop_bootsier::prelude::*;
|
|
||||||
// let bp = BreakPoint::MD;
|
|
||||||
// assert_eq!(bp.class_with("col", ""), "col-md");
|
|
||||||
// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
|
||||||
//
|
|
||||||
// let bp = BreakPoint::None;
|
|
||||||
// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
|
|
||||||
// assert_eq!(bp.class_with("col", "12"), "col-12");
|
|
||||||
//
|
|
||||||
// let bp = BreakPoint::LG;
|
|
||||||
// assert_eq!(bp.class_with("", "3"), "");
|
|
||||||
// ```
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn class_with(self, prefix: &str, suffix: &str) -> String {
|
|
||||||
if prefix.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let bp = self.as_str();
|
|
||||||
let has_bp = !bp.is_empty();
|
|
||||||
let has_suffix = !suffix.is_empty();
|
|
||||||
|
|
||||||
let mut len = prefix.len();
|
|
||||||
if has_bp {
|
|
||||||
len += 1 + bp.len();
|
|
||||||
}
|
|
||||||
if has_suffix {
|
|
||||||
len += 1 + suffix.len();
|
|
||||||
}
|
|
||||||
let mut class = String::with_capacity(len);
|
|
||||||
class.push_str(prefix);
|
|
||||||
if has_bp {
|
|
||||||
class.push('-');
|
|
||||||
class.push_str(bp);
|
|
||||||
}
|
|
||||||
if has_suffix {
|
|
||||||
class.push('-');
|
|
||||||
class.push_str(suffix);
|
|
||||||
}
|
|
||||||
class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::Color;
|
|
||||||
|
|
||||||
// **< ButtonColor >********************************************************************************
|
|
||||||
|
|
||||||
/// Variantes de color `btn-*` para botones.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ButtonColor {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Genera internamente clases `btn-{color}` (botón relleno).
|
|
||||||
Background(Color),
|
|
||||||
/// Genera `btn-outline-{color}` (fondo transparente y contorno con borde).
|
|
||||||
Outline(Color),
|
|
||||||
/// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto.
|
|
||||||
Link,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ButtonColor {
|
|
||||||
const BTN_PREFIX: &str = "btn-";
|
|
||||||
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
|
|
||||||
const BTN_LINK: &str = "btn-link";
|
|
||||||
|
|
||||||
// Añade la clase `btn-*` a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if let Self::Default = self {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::Default => unreachable!(),
|
|
||||||
Self::Background(c) => {
|
|
||||||
classes.push_str(Self::BTN_PREFIX);
|
|
||||||
classes.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
Self::Outline(c) => {
|
|
||||||
classes.push_str(Self::BTN_OUTLINE_PREFIX);
|
|
||||||
classes.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
Self::Link => {
|
|
||||||
classes.push_str(Self::BTN_LINK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `btn-*` correspondiente al color del botón.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(
|
|
||||||
/// ButtonColor::Background(Color::Primary).to_class(),
|
|
||||||
/// "btn-primary"
|
|
||||||
/// );
|
|
||||||
/// assert_eq!(
|
|
||||||
/// ButtonColor::Outline(Color::Danger).to_class(),
|
|
||||||
/// "btn-outline-danger"
|
|
||||||
/// );
|
|
||||||
/// assert_eq!(ButtonColor::Link.to_class(), "btn-link");
|
|
||||||
/// assert_eq!(ButtonColor::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Default => String::new(),
|
|
||||||
Self::Background(c) => {
|
|
||||||
let color = c.as_str();
|
|
||||||
let mut class = String::with_capacity(Self::BTN_PREFIX.len() + color.len());
|
|
||||||
class.push_str(Self::BTN_PREFIX);
|
|
||||||
class.push_str(color);
|
|
||||||
class
|
|
||||||
}
|
|
||||||
Self::Outline(c) => {
|
|
||||||
let color = c.as_str();
|
|
||||||
let mut class = String::with_capacity(Self::BTN_OUTLINE_PREFIX.len() + color.len());
|
|
||||||
class.push_str(Self::BTN_OUTLINE_PREFIX);
|
|
||||||
class.push_str(color);
|
|
||||||
class
|
|
||||||
}
|
|
||||||
Self::Link => Self::BTN_LINK.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< ButtonSize >*********************************************************************************
|
|
||||||
|
|
||||||
/// Tamaño visual de un botón.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ButtonSize {
|
|
||||||
/// Tamaño por defecto del tema (no añade clase).
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Botón compacto.
|
|
||||||
Small,
|
|
||||||
/// Botón destacado/grande.
|
|
||||||
Large,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ButtonSize {
|
|
||||||
const BTN_SM: &str = "btn-sm";
|
|
||||||
const BTN_LG: &str = "btn-lg";
|
|
||||||
|
|
||||||
// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if let Self::Default = self {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::Default => unreachable!(),
|
|
||||||
Self::Small => classes.push_str(Self::BTN_SM),
|
|
||||||
Self::Large => classes.push_str(Self::BTN_LG),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
|
|
||||||
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
|
||||||
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Default => String::new(),
|
|
||||||
Self::Small => Self::BTN_SM.to_string(),
|
|
||||||
Self::Large => Self::BTN_LG.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,375 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Color >**************************************************************************************
|
|
||||||
|
|
||||||
/// Paleta de colores temáticos.
|
|
||||||
///
|
|
||||||
/// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`,
|
|
||||||
/// etc.). Este tipo enumerado sirve de base para componer las clases de color para fondo
|
|
||||||
/// ([`classes::Background`](crate::theme::classes::Background)), bordes
|
|
||||||
/// ([`classes::Border`](crate::theme::classes::Border)) y texto
|
|
||||||
/// ([`classes::Text`](crate::theme::classes::Text)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Color {
|
|
||||||
#[default]
|
|
||||||
Primary,
|
|
||||||
Secondary,
|
|
||||||
Success,
|
|
||||||
Info,
|
|
||||||
Warning,
|
|
||||||
Danger,
|
|
||||||
Light,
|
|
||||||
Dark,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Color {
|
|
||||||
// Devuelve el nombre del color.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Primary => "primary",
|
|
||||||
Self::Secondary => "secondary",
|
|
||||||
Self::Success => "success",
|
|
||||||
Self::Info => "info",
|
|
||||||
Self::Warning => "warning",
|
|
||||||
Self::Danger => "danger",
|
|
||||||
Self::Light => "light",
|
|
||||||
Self::Dark => "dark",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade el nombre del color a la cadena de clases (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(self.as_str());
|
|
||||||
} */
|
|
||||||
|
|
||||||
/// Devuelve la clase correspondiente al color.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(Color::Primary.to_class(), "primary");
|
|
||||||
/// assert_eq!(Color::Danger.to_class(), "danger");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Opacity >************************************************************************************
|
|
||||||
|
|
||||||
/// Niveles de opacidad (`opacity-*`).
|
|
||||||
///
|
|
||||||
/// Se usa normalmente para graduar la transparencia del color de fondo `bg-opacity-*`
|
|
||||||
/// ([`classes::Background`](crate::theme::classes::Background)), de los bordes `border-opacity-*`
|
|
||||||
/// ([`classes::Border`](crate::theme::classes::Border)) o del texto `text-opacity-*`
|
|
||||||
/// ([`classes::Text`](crate::theme::classes::Text)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Opacity {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Permite generar clases `*-opacity-100` (100% de opacidad).
|
|
||||||
Opaque,
|
|
||||||
/// Permite generar clases `*-opacity-75` (75%).
|
|
||||||
SemiOpaque,
|
|
||||||
/// Permite generar clases `*-opacity-50` (50%).
|
|
||||||
Half,
|
|
||||||
/// Permite generar clases `*-opacity-25` (25%).
|
|
||||||
SemiTransparent,
|
|
||||||
/// Permite generar clases `*-opacity-10` (10%).
|
|
||||||
AlmostTransparent,
|
|
||||||
/// Permite generar clases `*-opacity-0` (0%, totalmente transparente).
|
|
||||||
Transparent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Opacity {
|
|
||||||
const OPACITY: &str = "opacity";
|
|
||||||
const OPACITY_PREFIX: &str = "-opacity";
|
|
||||||
|
|
||||||
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Default => None,
|
|
||||||
Self::Opaque => Some("-100"),
|
|
||||||
Self::SemiOpaque => Some("-75"),
|
|
||||||
Self::Half => Some("-50"),
|
|
||||||
Self::SemiTransparent => Some("-25"),
|
|
||||||
Self::AlmostTransparent => Some("-10"),
|
|
||||||
Self::Transparent => Some("-0"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
|
|
||||||
// vacío para `opacity-*`).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
if prefix.is_empty() {
|
|
||||||
classes.push_str(Self::OPACITY);
|
|
||||||
} else {
|
|
||||||
classes.push_str(prefix);
|
|
||||||
classes.push_str(Self::OPACITY_PREFIX);
|
|
||||||
}
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
|
|
||||||
// `opacity-*`).
|
|
||||||
//
|
|
||||||
// # Ejemplos
|
|
||||||
//
|
|
||||||
// ```rust
|
|
||||||
// # use pagetop_bootsier::prelude::*;
|
|
||||||
// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
|
||||||
// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
|
||||||
// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
|
||||||
// assert_eq!(Opacity::Default.class_with("bg"), "");
|
|
||||||
// ```
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let base_len = if prefix.is_empty() {
|
|
||||||
Self::OPACITY.len()
|
|
||||||
} else {
|
|
||||||
prefix.len() + Self::OPACITY_PREFIX.len()
|
|
||||||
};
|
|
||||||
let mut class = String::with_capacity(base_len + suffix.len());
|
|
||||||
if prefix.is_empty() {
|
|
||||||
class.push_str(Self::OPACITY);
|
|
||||||
} else {
|
|
||||||
class.push_str(prefix);
|
|
||||||
class.push_str(Self::OPACITY_PREFIX);
|
|
||||||
}
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase de opacidad `opacity-*`.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
|
|
||||||
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
|
|
||||||
/// assert_eq!(Opacity::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
self.class_with("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< ColorBg >************************************************************************************
|
|
||||||
|
|
||||||
/// Colores `bg-*` para el fondo.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ColorBg {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Fondo predefinido del tema (`bg-body`).
|
|
||||||
Body,
|
|
||||||
/// Fondo predefinido del tema (`bg-body-secondary`).
|
|
||||||
BodySecondary,
|
|
||||||
/// Fondo predefinido del tema (`bg-body-tertiary`).
|
|
||||||
BodyTertiary,
|
|
||||||
/// Genera internamente clases `bg-{color}` (p. ej., `bg-primary`).
|
|
||||||
Theme(Color),
|
|
||||||
/// Genera internamente clases `bg-{color}-subtle` (un tono suavizado del color).
|
|
||||||
Subtle(Color),
|
|
||||||
/// Color negro.
|
|
||||||
Black,
|
|
||||||
/// Color blanco.
|
|
||||||
White,
|
|
||||||
/// No aplica ningún color de fondo (`bg-transparent`).
|
|
||||||
Transparent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorBg {
|
|
||||||
const BG: &str = "bg";
|
|
||||||
const BG_PREFIX: &str = "bg-";
|
|
||||||
|
|
||||||
// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Default => None,
|
|
||||||
Self::Body => Some("-body"),
|
|
||||||
Self::BodySecondary => Some("-body-secondary"),
|
|
||||||
Self::BodyTertiary => Some("-body-tertiary"),
|
|
||||||
Self::Theme(_) => Some(""),
|
|
||||||
Self::Subtle(_) => Some("-subtle"),
|
|
||||||
Self::Black => Some("-black"),
|
|
||||||
Self::White => Some("-white"),
|
|
||||||
Self::Transparent => Some("-transparent"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase de fondo `bg-*` a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => {
|
|
||||||
classes.push_str(Self::BG_PREFIX);
|
|
||||||
classes.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => classes.push_str(Self::BG),
|
|
||||||
}
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `bg-*` correspondiente al fondo.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
|
|
||||||
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
|
|
||||||
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
|
|
||||||
/// assert_eq!(ColorBg::Transparent.to_class(), "bg-transparent");
|
|
||||||
/// assert_eq!(ColorBg::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let base_len = match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => Self::BG_PREFIX.len() + c.as_str().len(),
|
|
||||||
_ => Self::BG.len(),
|
|
||||||
};
|
|
||||||
let mut class = String::with_capacity(base_len + suffix.len());
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Subtle(c) => {
|
|
||||||
class.push_str(Self::BG_PREFIX);
|
|
||||||
class.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => class.push_str(Self::BG),
|
|
||||||
}
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< ColorText >**********************************************************************************
|
|
||||||
|
|
||||||
/// Colores `text-*` para el texto.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ColorText {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Color predefinido del tema (`text-body`).
|
|
||||||
Body,
|
|
||||||
/// Color predefinido del tema (`text-body-emphasis`).
|
|
||||||
BodyEmphasis,
|
|
||||||
/// Color predefinido del tema (`text-body-secondary`).
|
|
||||||
BodySecondary,
|
|
||||||
/// Color predefinido del tema (`text-body-tertiary`).
|
|
||||||
BodyTertiary,
|
|
||||||
/// Genera internamente clases `text-{color}`.
|
|
||||||
Theme(Color),
|
|
||||||
/// Genera internamente clases `text-{color}-emphasis` (mayor contraste acorde al tema).
|
|
||||||
Emphasis(Color),
|
|
||||||
/// Color negro.
|
|
||||||
Black,
|
|
||||||
/// Color blanco.
|
|
||||||
White,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorText {
|
|
||||||
const TEXT: &str = "text";
|
|
||||||
const TEXT_PREFIX: &str = "text-";
|
|
||||||
|
|
||||||
// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Default => None,
|
|
||||||
Self::Body => Some("-body"),
|
|
||||||
Self::BodyEmphasis => Some("-body-emphasis"),
|
|
||||||
Self::BodySecondary => Some("-body-secondary"),
|
|
||||||
Self::BodyTertiary => Some("-body-tertiary"),
|
|
||||||
Self::Theme(_) => Some(""),
|
|
||||||
Self::Emphasis(_) => Some("-emphasis"),
|
|
||||||
Self::Black => Some("-black"),
|
|
||||||
Self::White => Some("-white"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase de texto `text-*` a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Emphasis(c) => {
|
|
||||||
classes.push_str(Self::TEXT_PREFIX);
|
|
||||||
classes.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => classes.push_str(Self::TEXT),
|
|
||||||
}
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `text-*` correspondiente al color del texto.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(ColorText::Body.to_class(), "text-body");
|
|
||||||
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
|
|
||||||
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
|
|
||||||
/// assert_eq!(ColorText::Black.to_class(), "text-black");
|
|
||||||
/// assert_eq!(ColorText::Default.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let base_len = match self {
|
|
||||||
Self::Theme(c) | Self::Emphasis(c) => Self::TEXT_PREFIX.len() + c.as_str().len(),
|
|
||||||
_ => Self::TEXT.len(),
|
|
||||||
};
|
|
||||||
let mut class = String::with_capacity(base_len + suffix.len());
|
|
||||||
match self {
|
|
||||||
Self::Theme(c) | Self::Emphasis(c) => {
|
|
||||||
class.push_str(Self::TEXT_PREFIX);
|
|
||||||
class.push_str(c.as_str());
|
|
||||||
}
|
|
||||||
_ => class.push_str(Self::TEXT),
|
|
||||||
}
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< ScaleSize >**********************************************************************************
|
|
||||||
|
|
||||||
/// Escala discreta de tamaños para definir clases utilitarias.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ScaleSize {
|
|
||||||
/// Sin tamaño (no define ninguna clase).
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
/// Tamaño automático.
|
|
||||||
Auto,
|
|
||||||
/// Escala cero.
|
|
||||||
Zero,
|
|
||||||
/// Escala uno.
|
|
||||||
One,
|
|
||||||
/// Escala dos.
|
|
||||||
Two,
|
|
||||||
/// Escala tres.
|
|
||||||
Three,
|
|
||||||
/// Escala cuatro.
|
|
||||||
Four,
|
|
||||||
/// Escala cinco.
|
|
||||||
Five,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScaleSize {
|
|
||||||
// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
|
|
||||||
// clase, o `""` para el tamaño automático.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::None => None,
|
|
||||||
Self::Auto => Some(""),
|
|
||||||
Self::Zero => Some("-0"),
|
|
||||||
Self::One => Some("-1"),
|
|
||||||
Self::Two => Some("-2"),
|
|
||||||
Self::Three => Some("-3"),
|
|
||||||
Self::Four => Some("-4"),
|
|
||||||
Self::Five => Some("-5"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade el tamaño a la cadena de clases usando el prefijo dado.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
|
||||||
if !prefix.is_empty() {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(prefix);
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
|
|
||||||
//
|
|
||||||
// # Ejemplo
|
|
||||||
//
|
|
||||||
// ```rust
|
|
||||||
// # use pagetop_bootsier::prelude::*;
|
|
||||||
// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
|
||||||
// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
|
||||||
// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
|
||||||
// assert_eq!(ScaleSize::None.class_with("border"), "");
|
|
||||||
// ```
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
|
||||||
if !prefix.is_empty() {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let mut class = String::with_capacity(prefix.len() + suffix.len());
|
|
||||||
class.push_str(prefix);
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Side >***************************************************************************************
|
|
||||||
|
|
||||||
/// Lados sobre los que aplicar una clase utilitaria (respetando LTR/RTL).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Side {
|
|
||||||
/// Todos los lados.
|
|
||||||
#[default]
|
|
||||||
All,
|
|
||||||
/// Lado superior.
|
|
||||||
Top,
|
|
||||||
/// Lado inferior.
|
|
||||||
Bottom,
|
|
||||||
/// Lado lógico de inicio (respetando RTL).
|
|
||||||
Start,
|
|
||||||
/// Lado lógico de fin (respetando RTL).
|
|
||||||
End,
|
|
||||||
/// Lados lógicos laterales (abreviatura *x*).
|
|
||||||
LeftAndRight,
|
|
||||||
/// Lados superior e inferior (abreviatura *y*).
|
|
||||||
TopAndBottom,
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
/// Radio para el redondeo de esquinas ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum RoundedRadius {
|
|
||||||
/// No define ninguna clase.
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
/// Genera `rounded` (radio por defecto del tema).
|
|
||||||
Default,
|
|
||||||
/// Genera `rounded-0` (sin redondeo).
|
|
||||||
Zero,
|
|
||||||
/// Genera `rounded-1`.
|
|
||||||
Scale1,
|
|
||||||
/// Genera `rounded-2`.
|
|
||||||
Scale2,
|
|
||||||
/// Genera `rounded-3`.
|
|
||||||
Scale3,
|
|
||||||
/// Genera `rounded-4`.
|
|
||||||
Scale4,
|
|
||||||
/// Genera `rounded-5`.
|
|
||||||
Scale5,
|
|
||||||
/// Genera `rounded-circle`.
|
|
||||||
Circle,
|
|
||||||
/// Genera `rounded-pill`.
|
|
||||||
Pill,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoundedRadius {
|
|
||||||
const ROUNDED: &str = "rounded";
|
|
||||||
|
|
||||||
// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
|
|
||||||
// redondeo por defecto.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::None => None,
|
|
||||||
Self::Default => Some(""),
|
|
||||||
Self::Zero => Some("-0"),
|
|
||||||
Self::Scale1 => Some("-1"),
|
|
||||||
Self::Scale2 => Some("-2"),
|
|
||||||
Self::Scale3 => Some("-3"),
|
|
||||||
Self::Scale4 => Some("-4"),
|
|
||||||
Self::Scale5 => Some("-5"),
|
|
||||||
Self::Circle => Some("-circle"),
|
|
||||||
Self::Pill => Some("-pill"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
|
|
||||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
if prefix.is_empty() {
|
|
||||||
classes.push_str(Self::ROUNDED);
|
|
||||||
} else {
|
|
||||||
classes.push_str(prefix);
|
|
||||||
}
|
|
||||||
classes.push_str(suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
|
|
||||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
|
||||||
//
|
|
||||||
// # Ejemplos
|
|
||||||
//
|
|
||||||
// ```rust
|
|
||||||
// # use pagetop_bootsier::prelude::*;
|
|
||||||
// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
|
||||||
// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
|
||||||
// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
|
||||||
// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
|
|
||||||
// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
|
|
||||||
// ```
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
|
||||||
if let Some(suffix) = self.suffix() {
|
|
||||||
let base_len = if prefix.is_empty() {
|
|
||||||
Self::ROUNDED.len()
|
|
||||||
} else {
|
|
||||||
prefix.len()
|
|
||||||
};
|
|
||||||
let mut class = String::with_capacity(base_len + suffix.len());
|
|
||||||
if prefix.is_empty() {
|
|
||||||
class.push_str(Self::ROUNDED);
|
|
||||||
} else {
|
|
||||||
class.push_str(prefix);
|
|
||||||
}
|
|
||||||
class.push_str(suffix);
|
|
||||||
return class;
|
|
||||||
}
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase `rounded-*` para el redondeo de esquinas.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
|
|
||||||
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
|
|
||||||
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
|
|
||||||
/// assert_eq!(RoundedRadius::Circle.to_class(), "rounded-circle");
|
|
||||||
/// assert_eq!(RoundedRadius::None.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
self.class_with("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,175 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::{BorderColor, Opacity, ScaleSize, Side};
|
|
||||||
|
|
||||||
/// Clases para crear **bordes**.
|
|
||||||
///
|
|
||||||
/// Permite:
|
|
||||||
///
|
|
||||||
/// - Iniciar un borde sin tamaño inicial (`Border::default()`).
|
|
||||||
/// - Crear un borde con tamaño por defecto (`Border::new()`).
|
|
||||||
/// - Ajustar el tamaño de cada **lado lógico** (`side`, respetando LTR/RTL).
|
|
||||||
/// - Definir un tamaño **global** para todo el borde (`size`).
|
|
||||||
/// - Aplicar un **color** al borde (`BorderColor`).
|
|
||||||
/// - Aplicar un nivel de **opacidad** (`Opacity`).
|
|
||||||
///
|
|
||||||
/// # Comportamiento aditivo / sustractivo
|
|
||||||
///
|
|
||||||
/// - **Aditivo**: basta con crear un borde sin tamaño con `classes::Border::default()` para ir
|
|
||||||
/// añadiendo cada lado lógico con el tamaño deseado usando `ScaleSize::{One..Five}`.
|
|
||||||
///
|
|
||||||
/// - **Sustractivo**: se crea un borde con tamaño predefinido, p. ej. usando
|
|
||||||
/// `classes::Border::new()` o `classes::Border::with(ScaleSize::Two)` y eliminar los lados
|
|
||||||
/// deseados con `ScaleSize::Zero`.
|
|
||||||
///
|
|
||||||
/// - **Anchos diferentes por lado**: usando `ScaleSize::{Zero..Five}` en cada lado deseado.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// **Borde global:**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::with(ScaleSize::Two);
|
|
||||||
/// assert_eq!(b.to_class(), "border-2");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Aditivo (solo borde superior):**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
|
|
||||||
/// assert_eq!(b.to_class(), "border-top-1");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Sustractivo (borde global menos el superior):**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
|
|
||||||
/// assert_eq!(b.to_class(), "border border-top-0");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::default()
|
|
||||||
/// .with_side(Side::Start, ScaleSize::Two)
|
|
||||||
/// .with_side(Side::End, ScaleSize::Four);
|
|
||||||
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Combinado (ejemplo completo):**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let b = classes::Border::new() // Borde por defecto.
|
|
||||||
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
|
|
||||||
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
|
|
||||||
/// .with_color(BorderColor::Theme(Color::Primary))
|
|
||||||
/// .with_opacity(Opacity::Half);
|
|
||||||
///
|
|
||||||
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
|
|
||||||
/// ```
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Border {
|
|
||||||
all : ScaleSize,
|
|
||||||
top : ScaleSize,
|
|
||||||
end : ScaleSize,
|
|
||||||
bottom : ScaleSize,
|
|
||||||
start : ScaleSize,
|
|
||||||
color : BorderColor,
|
|
||||||
opacity: Opacity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Border {
|
|
||||||
/// Prepara un borde del tamaño predefinido. Equivale a `border` (ancho por defecto del tema).
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::with(ScaleSize::Auto)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un borde **con un tamaño global** (`size`).
|
|
||||||
pub fn with(size: ScaleSize) -> Self {
|
|
||||||
Self::default().with_side(Side::All, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Border BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
pub fn with_side(mut self, side: Side, size: ScaleSize) -> Self {
|
|
||||||
match side {
|
|
||||||
Side::All => self.all = size,
|
|
||||||
Side::Top => self.top = size,
|
|
||||||
Side::Bottom => self.bottom = size,
|
|
||||||
Side::Start => self.start = size,
|
|
||||||
Side::End => self.end = size,
|
|
||||||
Side::LeftAndRight => {
|
|
||||||
self.start = size;
|
|
||||||
self.end = size;
|
|
||||||
}
|
|
||||||
Side::TopAndBottom => {
|
|
||||||
self.top = size;
|
|
||||||
self.bottom = size;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el color del borde.
|
|
||||||
pub fn with_color(mut self, color: BorderColor) -> Self {
|
|
||||||
self.color = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la opacidad del borde.
|
|
||||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
|
||||||
self.opacity = opacity;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Border HELPERS >*************************************************************************
|
|
||||||
|
|
||||||
/// Añade las clases de borde a la cadena de clases.
|
|
||||||
///
|
|
||||||
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
|
|
||||||
/// *color* y *opacidad*; respetando LTR/RTL y omitiendo las definiciones vacías.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
self.all .push_class(classes, "border");
|
|
||||||
self.top .push_class(classes, "border-top");
|
|
||||||
self.end .push_class(classes, "border-end");
|
|
||||||
self.bottom .push_class(classes, "border-bottom");
|
|
||||||
self.start .push_class(classes, "border-start");
|
|
||||||
self.color .push_class(classes);
|
|
||||||
self.opacity.push_class(classes, "border");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve las clases de borde como cadena (`"border-2"`,
|
|
||||||
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
|
|
||||||
///
|
|
||||||
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Atajo para crear un [`classes::Border`](crate::theme::classes::Border) a partir de un tamaño
|
|
||||||
/// [`ScaleSize`] aplicado a todo el borde.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// // Convertir explícitamente con `From::from`:
|
|
||||||
/// let b = classes::Border::from(ScaleSize::Two);
|
|
||||||
/// assert_eq!(b.to_class(), "border-2");
|
|
||||||
///
|
|
||||||
/// // Convertir implícitamente con `into()`:
|
|
||||||
/// let b: classes::Border = ScaleSize::Auto.into();
|
|
||||||
/// assert_eq!(b.to_class(), "border");
|
|
||||||
/// ```
|
|
||||||
impl From<ScaleSize> for Border {
|
|
||||||
fn from(size: ScaleSize) -> Self {
|
|
||||||
Self::with(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::{ColorBg, ColorText, Opacity};
|
|
||||||
|
|
||||||
// **< Background >*********************************************************************************
|
|
||||||
|
|
||||||
/// Clases para establecer **color/opacidad del fondo**.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// // Sin clases.
|
|
||||||
/// let s = classes::Background::new();
|
|
||||||
/// assert_eq!(s.to_class(), "");
|
|
||||||
///
|
|
||||||
/// // Sólo color de fondo.
|
|
||||||
/// let s = classes::Background::with(ColorBg::Theme(Color::Primary));
|
|
||||||
/// assert_eq!(s.to_class(), "bg-primary");
|
|
||||||
///
|
|
||||||
/// // Color más opacidad.
|
|
||||||
/// let s = classes::Background::with(ColorBg::BodySecondary).with_opacity(Opacity::Half);
|
|
||||||
/// assert_eq!(s.to_class(), "bg-body-secondary bg-opacity-50");
|
|
||||||
///
|
|
||||||
/// // Usando `From<ColorBg>`.
|
|
||||||
/// let s: classes::Background = ColorBg::Black.into();
|
|
||||||
/// assert_eq!(s.to_class(), "bg-black");
|
|
||||||
///
|
|
||||||
/// // Usando `From<(ColorBg, Opacity)>`.
|
|
||||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
|
||||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Background {
|
|
||||||
color: ColorBg,
|
|
||||||
opacity: Opacity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Background {
|
|
||||||
/// Prepara un nuevo estilo para aplicar al fondo.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un estilo fijando el color de fondo (`bg-*`).
|
|
||||||
pub fn with(color: ColorBg) -> Self {
|
|
||||||
Self::default().with_color(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Background BUILDER >*********************************************************************
|
|
||||||
|
|
||||||
/// Establece el color de fondo (`bg-*`).
|
|
||||||
pub fn with_color(mut self, color: ColorBg) -> Self {
|
|
||||||
self.color = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la opacidad del fondo (`bg-opacity-*`).
|
|
||||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
|
||||||
self.opacity = opacity;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Background HELPERS >*********************************************************************
|
|
||||||
|
|
||||||
/// Añade las clases de fondo a la cadena de clases.
|
|
||||||
///
|
|
||||||
/// Concatena, en este orden, color del fondo (`bg-*`) y opacidad (`bg-opacity-*`),
|
|
||||||
/// omitiendo los fragmentos vacíos.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
self.color.push_class(classes);
|
|
||||||
self.opacity.push_class(classes, "bg");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
|
|
||||||
///
|
|
||||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(ColorBg, Opacity)> for Background {
|
|
||||||
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo y
|
|
||||||
/// la opacidad.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
|
||||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
|
||||||
/// ```
|
|
||||||
fn from((color, opacity): (ColorBg, Opacity)) -> Self {
|
|
||||||
Background::with(color).with_opacity(opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ColorBg> for Background {
|
|
||||||
/// Atajo para crear un [`classes::Background`](crate::theme::classes::Background) a partir del color de fondo.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let s: classes::Background = ColorBg::Black.into();
|
|
||||||
/// assert_eq!(s.to_class(), "bg-black");
|
|
||||||
/// ```
|
|
||||||
fn from(color: ColorBg) -> Self {
|
|
||||||
Background::with(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Text >***************************************************************************************
|
|
||||||
|
|
||||||
/// Clases para establecer **color/opacidad del texto**.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// // Sin clases.
|
|
||||||
/// let s = classes::Text::new();
|
|
||||||
/// assert_eq!(s.to_class(), "");
|
|
||||||
///
|
|
||||||
/// // Sólo color del texto.
|
|
||||||
/// let s = classes::Text::with(ColorText::Theme(Color::Primary));
|
|
||||||
/// assert_eq!(s.to_class(), "text-primary");
|
|
||||||
///
|
|
||||||
/// // Color del texto y opacidad.
|
|
||||||
/// let s = classes::Text::new().with_color(ColorText::White).with_opacity(Opacity::SemiTransparent);
|
|
||||||
/// assert_eq!(s.to_class(), "text-white text-opacity-25");
|
|
||||||
///
|
|
||||||
/// // Usando `From<ColorText>`.
|
|
||||||
/// let s: classes::Text = ColorText::Black.into();
|
|
||||||
/// assert_eq!(s.to_class(), "text-black");
|
|
||||||
///
|
|
||||||
/// // Usando `From<(ColorText, Opacity)>`.
|
|
||||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
|
||||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Text {
|
|
||||||
color: ColorText,
|
|
||||||
opacity: Opacity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Text {
|
|
||||||
/// Prepara un nuevo estilo para aplicar al texto.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un estilo fijando el color del texto (`text-*`).
|
|
||||||
pub fn with(color: ColorText) -> Self {
|
|
||||||
Self::default().with_color(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Text BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Establece el color del texto (`text-*`).
|
|
||||||
pub fn with_color(mut self, color: ColorText) -> Self {
|
|
||||||
self.color = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la opacidad del texto (`text-opacity-*`).
|
|
||||||
pub fn with_opacity(mut self, opacity: Opacity) -> Self {
|
|
||||||
self.opacity = opacity;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Text HELPERS >***************************************************************************
|
|
||||||
|
|
||||||
/// Añade las clases de texto a la cadena de clases.
|
|
||||||
///
|
|
||||||
/// Concatena, en este orden, `text-*` y `text-opacity-*`, omitiendo los fragmentos vacíos.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
self.color.push_class(classes);
|
|
||||||
self.opacity.push_class(classes, "text");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve las clases de texto como cadena (`"text-primary"`, `"text-white text-opacity-25"`,
|
|
||||||
/// etc.).
|
|
||||||
///
|
|
||||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(ColorText, Opacity)> for Text {
|
|
||||||
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
|
|
||||||
/// texto y su opacidad.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
|
||||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
|
||||||
/// ```
|
|
||||||
fn from((color, opacity): (ColorText, Opacity)) -> Self {
|
|
||||||
Text::with(color).with_opacity(opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ColorText> for Text {
|
|
||||||
/// Atajo para crear un [`classes::Text`](crate::theme::classes::Text) a partir del color del
|
|
||||||
/// texto.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let s: classes::Text = ColorText::Black.into();
|
|
||||||
/// assert_eq!(s.to_class(), "text-black");
|
|
||||||
/// ```
|
|
||||||
fn from(color: ColorText) -> Self {
|
|
||||||
Text::with(color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::{ScaleSize, Side};
|
|
||||||
use crate::theme::BreakPoint;
|
|
||||||
|
|
||||||
// **< Margin >*************************************************************************************
|
|
||||||
|
|
||||||
/// Clases para establecer **margin** por lado, tamaño y punto de ruptura.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
|
|
||||||
/// assert_eq!(m.to_class(), "mt-3");
|
|
||||||
///
|
|
||||||
/// let m = classes::Margin::with(Side::Start, ScaleSize::Auto).with_breakpoint(BreakPoint::LG);
|
|
||||||
/// assert_eq!(m.to_class(), "ms-lg-auto");
|
|
||||||
///
|
|
||||||
/// let m = classes::Margin::with(Side::All, ScaleSize::None);
|
|
||||||
/// assert_eq!(m.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Margin {
|
|
||||||
side: Side,
|
|
||||||
size: ScaleSize,
|
|
||||||
breakpoint: BreakPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Margin {
|
|
||||||
/// Crea un **margin** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
|
|
||||||
/// ruptura.
|
|
||||||
pub fn with(side: Side, size: ScaleSize) -> Self {
|
|
||||||
Margin {
|
|
||||||
side,
|
|
||||||
size,
|
|
||||||
breakpoint: BreakPoint::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Margin BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **margin**.
|
|
||||||
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
|
|
||||||
self.breakpoint = breakpoint;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Margin HELPERS >*************************************************************************
|
|
||||||
|
|
||||||
// Devuelve el prefijo `m*` según el lado.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn side_prefix(&self) -> &'static str {
|
|
||||||
match self.side {
|
|
||||||
Side::All => "m",
|
|
||||||
Side::Top => "mt",
|
|
||||||
Side::Bottom => "mb",
|
|
||||||
Side::Start => "ms",
|
|
||||||
Side::End => "me",
|
|
||||||
Side::LeftAndRight => "mx",
|
|
||||||
Side::TopAndBottom => "my",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn size_suffix(&self) -> Option<&'static str> {
|
|
||||||
match self.size {
|
|
||||||
ScaleSize::None => None,
|
|
||||||
ScaleSize::Auto => Some("auto"),
|
|
||||||
ScaleSize::Zero => Some("0"),
|
|
||||||
ScaleSize::One => Some("1"),
|
|
||||||
ScaleSize::Two => Some("2"),
|
|
||||||
ScaleSize::Three => Some("3"),
|
|
||||||
ScaleSize::Four => Some("4"),
|
|
||||||
ScaleSize::Five => Some("5"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade la clase de **margin** a la cadena de clases (reservado).
|
|
||||||
//
|
|
||||||
// No añade nada si `size` es `ScaleSize::None`.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let Some(size) = self.size_suffix() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.breakpoint
|
|
||||||
.push_class(classes, self.side_prefix(), size);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
|
|
||||||
///
|
|
||||||
/// Si `size` es `ScaleSize::None`, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let Some(size) = self.size_suffix() else {
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
self.breakpoint.class_with(self.side_prefix(), size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Padding >************************************************************************************
|
|
||||||
|
|
||||||
/// Clases para establecer **padding** por lado, tamaño y punto de ruptura.
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
|
|
||||||
/// assert_eq!(p.to_class(), "px-2");
|
|
||||||
///
|
|
||||||
/// let p = classes::Padding::with(Side::End, ScaleSize::Four).with_breakpoint(BreakPoint::SM);
|
|
||||||
/// assert_eq!(p.to_class(), "pe-sm-4");
|
|
||||||
///
|
|
||||||
/// let p = classes::Padding::with(Side::All, ScaleSize::Auto);
|
|
||||||
/// assert_eq!(p.to_class(), ""); // `Auto` no aplica a padding.
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Padding {
|
|
||||||
side: Side,
|
|
||||||
size: ScaleSize,
|
|
||||||
breakpoint: BreakPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Padding {
|
|
||||||
/// Crea un **padding** indicando lado(s) y tamaño. Por defecto no se aplica a ningún punto de
|
|
||||||
/// ruptura.
|
|
||||||
pub fn with(side: Side, size: ScaleSize) -> Self {
|
|
||||||
Padding {
|
|
||||||
side,
|
|
||||||
size,
|
|
||||||
breakpoint: BreakPoint::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Padding BUILDER >************************************************************************
|
|
||||||
|
|
||||||
/// Establece el punto de ruptura a partir del cual se empieza a aplicar el **padding**.
|
|
||||||
pub fn with_breakpoint(mut self, breakpoint: BreakPoint) -> Self {
|
|
||||||
self.breakpoint = breakpoint;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Padding HELPERS >************************************************************************
|
|
||||||
|
|
||||||
// Devuelve el prefijo `p*` según el lado.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn prefix(&self) -> &'static str {
|
|
||||||
match self.side {
|
|
||||||
Side::All => "p",
|
|
||||||
Side::Top => "pt",
|
|
||||||
Side::Bottom => "pb",
|
|
||||||
Side::Start => "ps",
|
|
||||||
Side::End => "pe",
|
|
||||||
Side::LeftAndRight => "px",
|
|
||||||
Side::TopAndBottom => "py",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
|
|
||||||
//
|
|
||||||
// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn suffix(&self) -> Option<&'static str> {
|
|
||||||
match self.size {
|
|
||||||
ScaleSize::None => None,
|
|
||||||
ScaleSize::Auto => None,
|
|
||||||
ScaleSize::Zero => Some("0"),
|
|
||||||
ScaleSize::One => Some("1"),
|
|
||||||
ScaleSize::Two => Some("2"),
|
|
||||||
ScaleSize::Three => Some("3"),
|
|
||||||
ScaleSize::Four => Some("4"),
|
|
||||||
ScaleSize::Five => Some("5"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade la clase de **padding** a la cadena de clases (reservado).
|
|
||||||
//
|
|
||||||
// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let Some(size) = self.suffix() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.breakpoint.push_class(classes, self.prefix(), size);
|
|
||||||
} */
|
|
||||||
|
|
||||||
// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
|
||||||
//
|
|
||||||
// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let Some(size) = self.suffix() else {
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
self.breakpoint.class_with(self.prefix(), size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::RoundedRadius;
|
|
||||||
|
|
||||||
/// Clases para definir **esquinas redondeadas**.
|
|
||||||
///
|
|
||||||
/// Permite:
|
|
||||||
///
|
|
||||||
/// - Definir un radio **global para todas las esquinas** (`radius`).
|
|
||||||
/// - Ajustar el radio asociado a las **esquinas de cada lado lógico** (`top`, `end`, `bottom`,
|
|
||||||
/// `start`, **en este orden**, respetando LTR/RTL).
|
|
||||||
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
|
|
||||||
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
|
||||||
///
|
|
||||||
/// **Radio global:**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::with(RoundedRadius::Default);
|
|
||||||
/// assert_eq!(r.to_class(), "rounded");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Sin redondeo:**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new();
|
|
||||||
/// assert_eq!(r.to_class(), "");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Radio en las esquinas de un lado lógico:**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
|
|
||||||
/// assert_eq!(r.to_class(), "rounded-end-2");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Radio en una esquina concreta:**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
|
|
||||||
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Combinado (ejemplo completo):**
|
|
||||||
/// ```rust
|
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
|
||||||
/// let r = classes::Rounded::new()
|
|
||||||
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
|
|
||||||
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
|
||||||
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
|
|
||||||
///
|
|
||||||
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
|
||||||
/// ```
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Rounded {
|
|
||||||
radius : RoundedRadius,
|
|
||||||
top : RoundedRadius,
|
|
||||||
end : RoundedRadius,
|
|
||||||
bottom : RoundedRadius,
|
|
||||||
start : RoundedRadius,
|
|
||||||
top_start : RoundedRadius,
|
|
||||||
top_end : RoundedRadius,
|
|
||||||
bottom_start: RoundedRadius,
|
|
||||||
bottom_end : RoundedRadius,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rounded {
|
|
||||||
/// Prepara las esquinas **sin redondeo global** de partida.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea las esquinas **con redondeo global** (`radius`).
|
|
||||||
pub fn with(radius: RoundedRadius) -> Self {
|
|
||||||
Self::default().with_radius(radius)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Rounded BUILDER >************************************************************************
|
|
||||||
|
|
||||||
/// Establece el radio global de las esquinas (`rounded*`).
|
|
||||||
pub fn with_radius(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.radius = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en las esquinas del lado superior (`rounded-top-*`).
|
|
||||||
pub fn with_top(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.top = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en las esquinas del lado lógico final (`rounded-end-*`). Respeta LTR/RTL.
|
|
||||||
pub fn with_end(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.end = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en las esquinas del lado inferior (`rounded-bottom-*`).
|
|
||||||
pub fn with_bottom(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.bottom = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en las esquinas del lado lógico inicial (`rounded-start-*`). Respeta
|
|
||||||
/// LTR/RTL.
|
|
||||||
pub fn with_start(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.start = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en la esquina superior-inicial (`rounded-top-start-*`). Respeta LTR/RTL.
|
|
||||||
pub fn with_top_start(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.top_start = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en la esquina superior-final (`rounded-top-end-*`). Respeta LTR/RTL.
|
|
||||||
pub fn with_top_end(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.top_end = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en la esquina inferior-inicial (`rounded-bottom-start-*`). Respeta
|
|
||||||
/// LTR/RTL.
|
|
||||||
pub fn with_bottom_start(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.bottom_start = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el radio en la esquina inferior-final (`rounded-bottom-end-*`). Respeta LTR/RTL.
|
|
||||||
pub fn with_bottom_end(mut self, radius: RoundedRadius) -> Self {
|
|
||||||
self.bottom_end = radius;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Rounded HELPERS >************************************************************************
|
|
||||||
|
|
||||||
/// Añade las clases de redondeo a la cadena de clases.
|
|
||||||
///
|
|
||||||
/// Concatena, en este orden, las clases para *global*, `top`, `end`, `bottom`, `start`,
|
|
||||||
/// `top-start`, `top-end`, `bottom-start` y `bottom-end`; respetando LTR/RTL y omitiendo las
|
|
||||||
/// definiciones vacías.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
self.radius .push_class(classes, "");
|
|
||||||
self.top .push_class(classes, "rounded-top");
|
|
||||||
self.end .push_class(classes, "rounded-end");
|
|
||||||
self.bottom .push_class(classes, "rounded-bottom");
|
|
||||||
self.start .push_class(classes, "rounded-start");
|
|
||||||
self.top_start .push_class(classes, "rounded-top-start");
|
|
||||||
self.top_end .push_class(classes, "rounded-top-end");
|
|
||||||
self.bottom_start.push_class(classes, "rounded-bottom-start");
|
|
||||||
self.bottom_end .push_class(classes, "rounded-bottom-end");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve las clases de redondeo como cadena (`"rounded"`,
|
|
||||||
/// `"rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"`, etc.).
|
|
||||||
///
|
|
||||||
/// Si no se define ningún radio, devuelve `""`.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,184 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para crear un **contenedor de componentes**.
|
|
||||||
///
|
|
||||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
|
||||||
/// si existen componentes hijos (*children*).
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Container {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
container_kind : container::Kind,
|
|
||||||
container_width: container::Width,
|
|
||||||
children : Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Container {
|
|
||||||
fn new() -> Self {
|
|
||||||
Container::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, self.width().to_class());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let output = self.children().render(cx);
|
|
||||||
if output.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
let style = match self.width() {
|
|
||||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
|
||||||
Some(join!("max-width: ", w.to_string(), ";"))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
match self.container_kind() {
|
|
||||||
container::Kind::Default => PrepareMarkup::With(html! {
|
|
||||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
container::Kind::Main => PrepareMarkup::With(html! {
|
|
||||||
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
container::Kind::Header => PrepareMarkup::With(html! {
|
|
||||||
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
container::Kind::Footer => PrepareMarkup::With(html! {
|
|
||||||
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
container::Kind::Section => PrepareMarkup::With(html! {
|
|
||||||
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
container::Kind::Article => PrepareMarkup::With(html! {
|
|
||||||
article id=[self.id()] class=[self.classes().get()] style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
/// Crea un contenedor de tipo `Main` (`<main>`).
|
|
||||||
pub fn main() -> Self {
|
|
||||||
Container {
|
|
||||||
container_kind: container::Kind::Main,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
|
||||||
pub fn header() -> Self {
|
|
||||||
Container {
|
|
||||||
container_kind: container::Kind::Header,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
|
||||||
pub fn footer() -> Self {
|
|
||||||
Container {
|
|
||||||
container_kind: container::Kind::Footer,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
|
||||||
pub fn section() -> Self {
|
|
||||||
Container {
|
|
||||||
container_kind: container::Kind::Section,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
|
||||||
pub fn article() -> Self {
|
|
||||||
Container {
|
|
||||||
container_kind: container::Kind::Article,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Container BUILDER >**********************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del contenedor.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al contenedor.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Modificar el color de fondo ([`classes::Background`]).
|
|
||||||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
|
||||||
/// - Establecer bordes ([`classes::Border`]).
|
|
||||||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el comportamiento del ancho para el contenedor.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_width(mut self, width: container::Width) -> Self {
|
|
||||||
self.container_width = width;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al contenedor.
|
|
||||||
#[inline]
|
|
||||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(component));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
|
||||||
self.children.alter_child(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Container GETTERS >**********************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al contenedor.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el tipo semántico del contenedor.
|
|
||||||
pub fn container_kind(&self) -> &container::Kind {
|
|
||||||
&self.container_kind
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
|
||||||
pub fn width(&self) -> &container::Width {
|
|
||||||
&self.container_width
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
|
||||||
pub fn children(&self) -> &Children {
|
|
||||||
&self.children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::aux::BreakPoint;
|
|
||||||
|
|
||||||
// **< Kind >***************************************************************************************
|
|
||||||
|
|
||||||
/// Tipo de contenedor ([`Container`](crate::theme::Container)).
|
|
||||||
///
|
|
||||||
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
|
|
||||||
/// común a todos los contenedores.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Kind {
|
|
||||||
/// Contenedor genérico (`<div>`).
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Contenido principal de la página (`<main>`).
|
|
||||||
Main,
|
|
||||||
/// Encabezado de la página o de sección (`<header>`).
|
|
||||||
Header,
|
|
||||||
/// Pie de la página o de sección (`<footer>`).
|
|
||||||
Footer,
|
|
||||||
/// Sección de contenido (`<section>`).
|
|
||||||
Section,
|
|
||||||
/// Artículo de contenido (`<article>`).
|
|
||||||
Article,
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Width >**************************************************************************************
|
|
||||||
|
|
||||||
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::Container)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Width {
|
|
||||||
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
|
|
||||||
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
|
||||||
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
|
||||||
From(BreakPoint),
|
|
||||||
/// Ocupa el 100% del ancho disponible siempre.
|
|
||||||
Fluid,
|
|
||||||
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
|
||||||
FluidMax(UnitValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Width {
|
|
||||||
const CONTAINER: &str = "container";
|
|
||||||
|
|
||||||
/* Añade el comportamiento del contenedor a la cadena de clases según ancho (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
match self {
|
|
||||||
Self::Default => BreakPoint::None.push_class(classes, Self::CONTAINER, ""),
|
|
||||||
Self::From(bp) => bp.push_class(classes, Self::CONTAINER, ""),
|
|
||||||
Self::Fluid | Self::FluidMax(_) => {
|
|
||||||
BreakPoint::None.push_class(classes, Self::CONTAINER, "fluid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
|
|
||||||
/// Devuelve la clase asociada al comportamiento del contenedor según el ajuste de su ancho.
|
|
||||||
#[inline]
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Default => BreakPoint::None.class_with(Self::CONTAINER, ""),
|
|
||||||
Self::From(bp) => bp.class_with(Self::CONTAINER, ""),
|
|
||||||
Self::Fluid | Self::FluidMax(_) => {
|
|
||||||
BreakPoint::None.class_with(Self::CONTAINER, "fluid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
//! Definiciones para crear menús desplegables [`Dropdown`].
|
|
||||||
//!
|
|
||||||
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
|
||||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
|
|
||||||
//! navegación, botones de acción, encabezados o divisores visuales.
|
|
||||||
//!
|
|
||||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
|
||||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
|
||||||
//!
|
|
||||||
//! # Ejemplo
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let dd = Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Menu"))
|
|
||||||
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
|
||||||
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
|
||||||
//! .with_direction(dropdown::Direction::Dropend)
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
|
||||||
//! .add_item(dropdown::Item::divider())
|
|
||||||
//! .add_item(dropdown::Item::header(L10n::n("User session")))
|
|
||||||
//! .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Dropdown;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::{Item, ItemKind};
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
/// Componente para crear un **menú desplegable**.
|
|
||||||
///
|
|
||||||
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
|
||||||
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la
|
|
||||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
|
||||||
/// apertura, alineación o política de cierre.
|
|
||||||
///
|
|
||||||
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
|
|
||||||
/// elementos sin ningún botón para interactuar.
|
|
||||||
///
|
|
||||||
/// Si este componente se usa en un menú [`Nav`] (ver [`nav::Item::dropdown()`]) sólo se tendrán en
|
|
||||||
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
|
|
||||||
/// resto de propiedades no afectarán a su representación en [`Nav`].
|
|
||||||
///
|
|
||||||
/// Ver ejemplo en el módulo [`dropdown`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Dropdown {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
title : L10n,
|
|
||||||
button_size : ButtonSize,
|
|
||||||
button_color : ButtonColor,
|
|
||||||
button_split : bool,
|
|
||||||
button_grouped: bool,
|
|
||||||
auto_close : dropdown::AutoClose,
|
|
||||||
direction : dropdown::Direction,
|
|
||||||
menu_align : dropdown::MenuAlign,
|
|
||||||
menu_position : dropdown::MenuPosition,
|
|
||||||
items : Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Dropdown {
|
|
||||||
fn new() -> Self {
|
|
||||||
Dropdown::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(
|
|
||||||
ClassesOp::Prepend,
|
|
||||||
self.direction().class_with(self.button_grouped()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
// Si no hay elementos en el menú, no se prepara.
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Título opcional para el menú desplegable.
|
|
||||||
let title = self.title().using(cx);
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
div id=[self.id()] class=[self.classes().get()] {
|
|
||||||
@if !title.is_empty() {
|
|
||||||
@let mut btn_classes = AttrClasses::new({
|
|
||||||
let mut classes = "btn".to_string();
|
|
||||||
self.button_size().push_class(&mut classes);
|
|
||||||
self.button_color().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
@let pos = self.menu_position();
|
|
||||||
@let offset = pos.data_offset();
|
|
||||||
@let reference = pos.data_reference();
|
|
||||||
@let auto_close = self.auto_close.as_str();
|
|
||||||
@let menu_classes = AttrClasses::new({
|
|
||||||
let mut classes = "dropdown-menu".to_string();
|
|
||||||
self.menu_align().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
|
|
||||||
// Renderizado en modo split (dos botones) o simple (un botón).
|
|
||||||
@if self.button_split() {
|
|
||||||
// Botón principal (acción/etiqueta).
|
|
||||||
@let btn = html! {
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class=[btn_classes.get()]
|
|
||||||
{
|
|
||||||
(title)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Botón *toggle* que abre/cierra el menú asociado.
|
|
||||||
@let btn_toggle = html! {
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class=[btn_classes.alter_value(
|
|
||||||
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
|
|
||||||
).get()]
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
data-bs-offset=[offset]
|
|
||||||
data-bs-reference=[reference]
|
|
||||||
data-bs-auto-close=[auto_close]
|
|
||||||
aria-expanded="false"
|
|
||||||
{
|
|
||||||
span class="visually-hidden" {
|
|
||||||
(L10n::t("dropdown_toggle", &LOCALES_BOOTSIER).using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes).
|
|
||||||
@match self.direction() {
|
|
||||||
dropdown::Direction::Dropstart => {
|
|
||||||
(btn_toggle)
|
|
||||||
ul class=[menu_classes.get()] { (items) }
|
|
||||||
(btn)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
(btn)
|
|
||||||
(btn_toggle)
|
|
||||||
ul class=[menu_classes.get()] { (items) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Botón único con funcionalidad de *toggle*.
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class=[btn_classes.alter_value(
|
|
||||||
ClassesOp::Add, "dropdown-toggle"
|
|
||||||
).get()]
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
data-bs-offset=[offset]
|
|
||||||
data-bs-reference=[reference]
|
|
||||||
data-bs-auto-close=[auto_close]
|
|
||||||
aria-expanded="false"
|
|
||||||
{
|
|
||||||
(title)
|
|
||||||
}
|
|
||||||
ul class=[menu_classes.get()] { (items) }
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Sin botón: sólo el listado como menú contextual.
|
|
||||||
ul class="dropdown-menu" { (items) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dropdown {
|
|
||||||
// **< Dropdown BUILDER >***********************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del menú desplegable.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al menú desplegable.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el título del menú desplegable.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_title(mut self, title: L10n) -> Self {
|
|
||||||
self.title = title;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ajusta el tamaño del botón.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_button_size(mut self, size: ButtonSize) -> Self {
|
|
||||||
self.button_size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el color/estilo del botón.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_button_color(mut self, color: ButtonColor) -> Self {
|
|
||||||
self.button_color = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Activa/desactiva el modo *split* (botón de acción + *toggle*).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_button_split(mut self, split: bool) -> Self {
|
|
||||||
self.button_split = split;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indica si el botón del menú está integrado en un grupo de botones.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_button_grouped(mut self, grouped: bool) -> Self {
|
|
||||||
self.button_grouped = grouped;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la política de cierre automático del menú desplegable.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_auto_close(mut self, auto_close: dropdown::AutoClose) -> Self {
|
|
||||||
self.auto_close = auto_close;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la dirección de despliegue del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_direction(mut self, direction: dropdown::Direction) -> Self {
|
|
||||||
self.direction = direction;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_menu_align(mut self, align: dropdown::MenuAlign) -> Self {
|
|
||||||
self.menu_align = align;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configura la posición del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_menu_position(mut self, position: dropdown::MenuPosition) -> Self {
|
|
||||||
self.menu_position = position;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo elemento hijo al menú.
|
|
||||||
#[inline]
|
|
||||||
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
|
||||||
self.items.add(Child::with(item));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
|
||||||
self.items.alter_typed(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Dropdown GETTERS >***********************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el título del menú desplegable.
|
|
||||||
pub fn title(&self) -> &L10n {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el tamaño configurado del botón.
|
|
||||||
pub fn button_size(&self) -> &ButtonSize {
|
|
||||||
&self.button_size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el color/estilo configurado del botón.
|
|
||||||
pub fn button_color(&self) -> &ButtonColor {
|
|
||||||
&self.button_color
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
|
||||||
pub fn button_split(&self) -> bool {
|
|
||||||
self.button_split
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
|
||||||
pub fn button_grouped(&self) -> bool {
|
|
||||||
self.button_grouped
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la política de cierre automático del menú desplegado.
|
|
||||||
pub fn auto_close(&self) -> &dropdown::AutoClose {
|
|
||||||
&self.auto_close
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la dirección de despliegue configurada.
|
|
||||||
pub fn direction(&self) -> &dropdown::Direction {
|
|
||||||
&self.direction
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
|
||||||
pub fn menu_align(&self) -> &dropdown::MenuAlign {
|
|
||||||
&self.menu_align
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la posición configurada para el menú desplegable.
|
|
||||||
pub fn menu_position(&self) -> &dropdown::MenuPosition {
|
|
||||||
&self.menu_position
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la lista de elementos (`children`) del menú.
|
|
||||||
pub fn items(&self) -> &Children {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< ItemKind >***********************************************************************************
|
|
||||||
|
|
||||||
/// Tipos de [`dropdown::Item`](crate::theme::dropdown::Item) disponibles en un menú desplegable
|
|
||||||
/// [`Dropdown`](crate::theme::Dropdown).
|
|
||||||
///
|
|
||||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
|
||||||
/// con él.
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum ItemKind {
|
|
||||||
/// Elemento vacío, no produce salida.
|
|
||||||
#[default]
|
|
||||||
Void,
|
|
||||||
/// Etiqueta sin comportamiento interactivo.
|
|
||||||
Label(L10n),
|
|
||||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
|
||||||
/// inicialmente deshabilitado.
|
|
||||||
Link {
|
|
||||||
label: L10n,
|
|
||||||
path: FnPathByContext,
|
|
||||||
blank: bool,
|
|
||||||
disabled: bool,
|
|
||||||
},
|
|
||||||
/// Acción ejecutable en la propia página, sin navegación asociada. Inicialmente puede estar
|
|
||||||
/// deshabilitado.
|
|
||||||
Button { label: L10n, disabled: bool },
|
|
||||||
/// Título o encabezado que separa grupos de opciones.
|
|
||||||
Header(L10n),
|
|
||||||
/// Separador visual entre bloques de elementos.
|
|
||||||
Divider,
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item >***************************************************************************************
|
|
||||||
|
|
||||||
/// Representa un **elemento individual** de un menú desplegable
|
|
||||||
/// [`Dropdown`](crate::theme::Dropdown).
|
|
||||||
///
|
|
||||||
/// Cada instancia de [`dropdown::Item`](crate::theme::dropdown::Item) se traduce en un componente
|
|
||||||
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, según su
|
|
||||||
/// [`ItemKind`].
|
|
||||||
///
|
|
||||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
|
||||||
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Item {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
item_kind: ItemKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Item {
|
|
||||||
fn new() -> Self {
|
|
||||||
Item::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
match self.item_kind() {
|
|
||||||
ItemKind::Void => PrepareMarkup::None,
|
|
||||||
|
|
||||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
span class="dropdown-item-text" {
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank,
|
|
||||||
disabled,
|
|
||||||
} => {
|
|
||||||
let path = path(cx);
|
|
||||||
let current_path = cx.request().map(|request| request.path());
|
|
||||||
let is_current = !*disabled && (current_path == Some(path));
|
|
||||||
|
|
||||||
let mut classes = "dropdown-item".to_string();
|
|
||||||
if is_current {
|
|
||||||
classes.push_str(" active");
|
|
||||||
}
|
|
||||||
if *disabled {
|
|
||||||
classes.push_str(" disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
let href = (!disabled).then_some(path);
|
|
||||||
let target = (!disabled && *blank).then_some("_blank");
|
|
||||||
let rel = (!disabled && *blank).then_some("noopener noreferrer");
|
|
||||||
|
|
||||||
let aria_current = (href.is_some() && is_current).then_some("page");
|
|
||||||
let aria_disabled = disabled.then_some("true");
|
|
||||||
let tabindex = disabled.then_some("-1");
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
a
|
|
||||||
class=(classes)
|
|
||||||
href=[href]
|
|
||||||
target=[target]
|
|
||||||
rel=[rel]
|
|
||||||
aria-current=[aria_current]
|
|
||||||
aria-disabled=[aria_disabled]
|
|
||||||
tabindex=[tabindex]
|
|
||||||
{
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemKind::Button { label, disabled } => {
|
|
||||||
let mut classes = "dropdown-item".to_string();
|
|
||||||
if *disabled {
|
|
||||||
classes.push_str(" disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
let aria_disabled = disabled.then_some("true");
|
|
||||||
let disabled_attr = disabled.then_some("disabled");
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
button
|
|
||||||
class=(classes)
|
|
||||||
type="button"
|
|
||||||
aria-disabled=[aria_disabled]
|
|
||||||
disabled=[disabled_attr]
|
|
||||||
{
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemKind::Header(label) => PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
h6 class="dropdown-header" {
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
ItemKind::Divider => PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
|
||||||
pub fn label(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Label(label),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace para la navegación.
|
|
||||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
|
||||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: false,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
|
||||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: true,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
|
||||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: true,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un botón de acción local, sin navegación asociada.
|
|
||||||
pub fn button(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Button {
|
|
||||||
label,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un botón deshabilitado.
|
|
||||||
pub fn button_disabled(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Button {
|
|
||||||
label,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un encabezado para un grupo de elementos dentro del menú.
|
|
||||||
pub fn header(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Header(label),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un separador visual entre bloques de elementos.
|
|
||||||
pub fn divider() -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Divider,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del elemento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al elemento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item GETTERS >***************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al elemento.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el tipo de elemento representado.
|
|
||||||
pub fn item_kind(&self) -> &ItemKind {
|
|
||||||
&self.item_kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
// **< AutoClose >**********************************************************************************
|
|
||||||
|
|
||||||
/// Estrategia para el cierre automático de un menú [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Define cuándo se cierra el menú desplegado según la interacción del usuario.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum AutoClose {
|
|
||||||
/// Comportamiento por defecto, se cierra con clics dentro y fuera del menú, o pulsando `Esc`.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Sólo se cierra con clics dentro del menú.
|
|
||||||
ClickableInside,
|
|
||||||
/// Sólo se cierra con clics fuera del menú.
|
|
||||||
ClickableOutside,
|
|
||||||
/// Cierre manual, no se cierra con clics; sólo al pulsar nuevamente el botón del menú
|
|
||||||
/// (*toggle*), o pulsando `Esc`.
|
|
||||||
ManualClose,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AutoClose {
|
|
||||||
// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) const fn as_str(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Default => None,
|
|
||||||
Self::ClickableInside => Some("inside"),
|
|
||||||
Self::ClickableOutside => Some("outside"),
|
|
||||||
Self::ManualClose => Some("false"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Direction >**********************************************************************************
|
|
||||||
|
|
||||||
/// Dirección de despliegue de un menú [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Controla desde qué posición se muestra el menú respecto al botón.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Direction {
|
|
||||||
/// Comportamiento por defecto (despliega el menú hacia abajo desde la posición inicial,
|
|
||||||
/// respetando LTR/RTL).
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Centra horizontalmente el menú respecto al botón.
|
|
||||||
Centered,
|
|
||||||
/// Despliega el menú hacia arriba.
|
|
||||||
Dropup,
|
|
||||||
/// Despliega el menú hacia arriba y centrado.
|
|
||||||
DropupCentered,
|
|
||||||
/// Despliega el menú desde el lateral final, respetando LTR/RTL.
|
|
||||||
Dropend,
|
|
||||||
/// Despliega el menú desde el lateral inicial, respetando LTR/RTL.
|
|
||||||
Dropstart,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Direction {
|
|
||||||
// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
|
|
||||||
#[rustfmt::skip ]
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self, grouped: bool) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Default if grouped => "",
|
|
||||||
Self::Default => "dropdown",
|
|
||||||
Self::Centered => "dropdown-center",
|
|
||||||
Self::Dropup => "dropup",
|
|
||||||
Self::DropupCentered => "dropup-center",
|
|
||||||
Self::Dropend => "dropend",
|
|
||||||
Self::Dropstart => "dropstart",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
|
|
||||||
// otros menús [`Dropdown`].
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
|
|
||||||
if grouped {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str("btn-group");
|
|
||||||
}
|
|
||||||
let class = self.as_str(grouped);
|
|
||||||
if !class.is_empty() {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
|
|
||||||
// [`Dropdown`], o `""` si no corresponde ninguna.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn class_with(self, grouped: bool) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes, grouped);
|
|
||||||
classes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< MenuAlign >**********************************************************************************
|
|
||||||
|
|
||||||
/// Alineación horizontal del menú desplegable [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
|
|
||||||
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum MenuAlign {
|
|
||||||
/// Alineación al inicio (comportamiento por defecto).
|
|
||||||
#[default]
|
|
||||||
Start,
|
|
||||||
/// Alineación al inicio a partir del punto de ruptura indicado.
|
|
||||||
StartAt(BreakPoint),
|
|
||||||
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
|
|
||||||
StartAndEnd(BreakPoint),
|
|
||||||
/// Alineación al final.
|
|
||||||
End,
|
|
||||||
/// Alineación al final a partir del punto de ruptura indicado.
|
|
||||||
EndAt(BreakPoint),
|
|
||||||
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
|
|
||||||
EndAndStart(BreakPoint),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MenuAlign {
|
|
||||||
#[inline]
|
|
||||||
fn push_one(classes: &mut String, class: &str) {
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
match self {
|
|
||||||
// Alineación por defecto (start), no añade clases extra.
|
|
||||||
Self::Start => {}
|
|
||||||
|
|
||||||
// `dropdown-menu-{bp}-start`
|
|
||||||
Self::StartAt(bp) => {
|
|
||||||
let class = bp.class_with("dropdown-menu", "start");
|
|
||||||
Self::push_one(classes, &class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `dropdown-menu-start` + `dropdown-menu-{bp}-end`
|
|
||||||
Self::StartAndEnd(bp) => {
|
|
||||||
Self::push_one(classes, "dropdown-menu-start");
|
|
||||||
let bp_class = bp.class_with("dropdown-menu", "end");
|
|
||||||
Self::push_one(classes, &bp_class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `dropdown-menu-end`
|
|
||||||
Self::End => {
|
|
||||||
Self::push_one(classes, "dropdown-menu-end");
|
|
||||||
}
|
|
||||||
|
|
||||||
// `dropdown-menu-{bp}-end`
|
|
||||||
Self::EndAt(bp) => {
|
|
||||||
let class = bp.class_with("dropdown-menu", "end");
|
|
||||||
Self::push_one(classes, &class);
|
|
||||||
}
|
|
||||||
|
|
||||||
// `dropdown-menu-end` + `dropdown-menu-{bp}-start`
|
|
||||||
Self::EndAndStart(bp) => {
|
|
||||||
Self::push_one(classes, "dropdown-menu-end");
|
|
||||||
let bp_class = bp.class_with("dropdown-menu", "start");
|
|
||||||
Self::push_one(classes, &bp_class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
let mut classes = String::new();
|
|
||||||
self.push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< MenuPosition >*******************************************************************************
|
|
||||||
|
|
||||||
/// Posición relativa del menú desplegable [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Permite indicar un desplazamiento (*offset*) manual o referenciar al elemento padre para el
|
|
||||||
/// cálculo de la posición.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum MenuPosition {
|
|
||||||
/// Posicionamiento automático por defecto.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Desplazamiento manual en píxeles `(x, y)` aplicado al menú. Se admiten valores negativos.
|
|
||||||
Offset(i8, i8),
|
|
||||||
/// Posiciona el menú tomando como referencia el botón padre. Especialmente útil cuando
|
|
||||||
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
|
|
||||||
Parent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MenuPosition {
|
|
||||||
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn data_offset(self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Offset(x, y) => Some(format!("{x},{y}")),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn data_reference(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Parent => Some("parent"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
|
||||||
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum IconKind {
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
Font(FontSize),
|
|
||||||
Svg {
|
|
||||||
shapes: Markup,
|
|
||||||
viewbox: AttrValue,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Icon {
|
|
||||||
classes : AttrClasses,
|
|
||||||
icon_kind : IconKind,
|
|
||||||
aria_label: AttrL10n,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Icon {
|
|
||||||
fn new() -> Self {
|
|
||||||
Icon::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
if !matches!(self.icon_kind(), IconKind::None) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
|
||||||
}
|
|
||||||
if let IconKind::Font(font_size) = self.icon_kind() {
|
|
||||||
self.alter_classes(ClassesOp::Add, font_size.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
match self.icon_kind() {
|
|
||||||
IconKind::None => PrepareMarkup::None,
|
|
||||||
IconKind::Font(_) => {
|
|
||||||
let aria_label = self.aria_label().lookup(cx);
|
|
||||||
let has_label = aria_label.is_some();
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
i
|
|
||||||
class=[self.classes().get()]
|
|
||||||
role=[has_label.then_some("img")]
|
|
||||||
aria-label=[aria_label]
|
|
||||||
aria-hidden=[(!has_label).then_some("true")]
|
|
||||||
{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
IconKind::Svg { shapes, viewbox } => {
|
|
||||||
let aria_label = self.aria_label().lookup(cx);
|
|
||||||
let has_label = aria_label.is_some();
|
|
||||||
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox=(viewbox)
|
|
||||||
fill="currentColor"
|
|
||||||
focusable="false"
|
|
||||||
class=[self.classes().get()]
|
|
||||||
role=[has_label.then_some("img")]
|
|
||||||
aria-label=[aria_label]
|
|
||||||
aria-hidden=[(!has_label).then_some("true")]
|
|
||||||
{
|
|
||||||
(shapes)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Icon {
|
|
||||||
pub fn font() -> Self {
|
|
||||||
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_sized(font_size: FontSize) -> Self {
|
|
||||||
Icon::default().with_icon_kind(IconKind::Font(font_size))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn svg(shapes: Markup) -> Self {
|
|
||||||
Icon::default().with_icon_kind(IconKind::Svg {
|
|
||||||
shapes,
|
|
||||||
viewbox: AttrValue::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
|
||||||
Icon::default().with_icon_kind(IconKind::Svg {
|
|
||||||
shapes,
|
|
||||||
viewbox: AttrValue::new(viewbox),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Icon BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al icono.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_icon_kind(mut self, icon_kind: IconKind) -> Self {
|
|
||||||
self.icon_kind = icon_kind;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_aria_label(mut self, label: L10n) -> Self {
|
|
||||||
self.aria_label.alter_value(label);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Icon GETTERS >***************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al icono.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_kind(&self) -> &IconKind {
|
|
||||||
&self.icon_kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aria_label(&self) -> &AttrL10n {
|
|
||||||
&self.aria_label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//! Definiciones para renderizar imágenes ([`Image`]).
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Size, Source};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Image;
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para renderizar una **imagen**.
|
|
||||||
///
|
|
||||||
/// - Ajusta su disposición según el origen definido en [`image::Source`].
|
|
||||||
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
|
||||||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
|
||||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
|
||||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Image {
|
|
||||||
id : AttrId,
|
|
||||||
classes: AttrClasses,
|
|
||||||
size : image::Size,
|
|
||||||
source : image::Source,
|
|
||||||
alt : AttrL10n,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Image {
|
|
||||||
fn new() -> Self {
|
|
||||||
Image::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let dimensions = self.size().to_style();
|
|
||||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
|
||||||
let is_decorative = alt_text.is_empty();
|
|
||||||
let source = match self.source() {
|
|
||||||
image::Source::Logo(logo) => {
|
|
||||||
return PrepareMarkup::With(html! {
|
|
||||||
span
|
|
||||||
id=[self.id()]
|
|
||||||
class=[self.classes().get()]
|
|
||||||
style=[dimensions]
|
|
||||||
role=[(!is_decorative).then_some("img")]
|
|
||||||
aria-label=[(!is_decorative).then_some(alt_text)]
|
|
||||||
aria-hidden=[is_decorative.then_some("true")]
|
|
||||||
{
|
|
||||||
(logo.render(cx))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
image::Source::Responsive(source) => Some(source),
|
|
||||||
image::Source::Thumbnail(source) => Some(source),
|
|
||||||
image::Source::Plain(source) => Some(source),
|
|
||||||
};
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
img
|
|
||||||
src=[source]
|
|
||||||
alt=(alt_text)
|
|
||||||
id=[self.id()]
|
|
||||||
class=[self.classes().get()]
|
|
||||||
style=[dimensions] {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Image {
|
|
||||||
/// Crea rápidamente una imagen especificando su origen.
|
|
||||||
pub fn with(source: image::Source) -> Self {
|
|
||||||
Image::default().with_source(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Image BUILDER >**************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la imagen.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas a la imagen.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Establecer bordes ([`classes::Border`]).
|
|
||||||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define las dimensiones de la imagen (auto, ancho/alto, ambos).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_size(mut self, size: image::Size) -> Self {
|
|
||||||
self.size = size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el origen de la imagen, influyendo en su disposición en el contenido.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_source(mut self, source: image::Source) -> Self {
|
|
||||||
self.source = source;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el texto alternativo localizado ([`L10n`]) para la imagen.
|
|
||||||
///
|
|
||||||
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
|
|
||||||
/// decorativa.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_alternative(mut self, alt: L10n) -> Self {
|
|
||||||
self.alt.alter_value(alt);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Image GETTERS >**************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas a la imagen.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve las dimensiones de la imagen.
|
|
||||||
pub fn size(&self) -> &image::Size {
|
|
||||||
&self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el origen de la imagen.
|
|
||||||
pub fn source(&self) -> &image::Source {
|
|
||||||
&self.source
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el texto alternativo localizado.
|
|
||||||
pub fn alternative(&self) -> &AttrL10n {
|
|
||||||
&self.alt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Size >***************************************************************************************
|
|
||||||
|
|
||||||
/// Define las **dimensiones** de una imagen ([`Image`](crate::theme::Image)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Size {
|
|
||||||
/// Ajuste automático por defecto.
|
|
||||||
///
|
|
||||||
/// La imagen usa su tamaño natural o se ajusta al contenedor donde se publica.
|
|
||||||
#[default]
|
|
||||||
Auto,
|
|
||||||
/// Establece explícitamente el **ancho y alto** de la imagen.
|
|
||||||
///
|
|
||||||
/// Útil cuando se desea fijar ambas dimensiones de forma exacta. Ten en cuenta que la imagen
|
|
||||||
/// puede distorsionarse si no se mantiene la proporción original.
|
|
||||||
Dimensions(UnitValue, UnitValue),
|
|
||||||
/// Establece sólo el **ancho** de la imagen.
|
|
||||||
///
|
|
||||||
/// La altura se ajusta proporcionalmente de manera automática.
|
|
||||||
Width(UnitValue),
|
|
||||||
/// Establece sólo la **altura** de la imagen.
|
|
||||||
///
|
|
||||||
/// El ancho se ajusta proporcionalmente de manera automática.
|
|
||||||
Height(UnitValue),
|
|
||||||
/// Establece **el mismo valor** para el ancho y el alto de la imagen.
|
|
||||||
///
|
|
||||||
/// Práctico para forzar rápidamente un área cuadrada. Ten en cuenta que la imagen puede
|
|
||||||
/// distorsionarse si la original no es cuadrada.
|
|
||||||
Both(UnitValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Size {
|
|
||||||
// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_style(self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Auto => None,
|
|
||||||
Self::Dimensions(w, h) => Some(format!("width: {w}; height: {h};")),
|
|
||||||
Self::Width(w) => Some(format!("width: {w};")),
|
|
||||||
Self::Height(h) => Some(format!("height: {h};")),
|
|
||||||
Self::Both(v) => Some(format!("width: {v}; height: {v};")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Source >*************************************************************************************
|
|
||||||
|
|
||||||
/// Especifica la **fuente** para publicar una imagen ([`Image`](crate::theme::Image)).
|
|
||||||
#[derive(AutoDefault, Clone, Debug, PartialEq)]
|
|
||||||
pub enum Source {
|
|
||||||
/// Imagen con el logotipo de PageTop.
|
|
||||||
#[default]
|
|
||||||
Logo(PageTopSvg),
|
|
||||||
/// Imagen que se adapta automáticamente a su contenedor.
|
|
||||||
///
|
|
||||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
|
||||||
Responsive(String),
|
|
||||||
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
|
||||||
///
|
|
||||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
|
||||||
Thumbnail(String),
|
|
||||||
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
|
|
||||||
///
|
|
||||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
|
||||||
Plain(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Source {
|
|
||||||
const IMG_FLUID: &str = "img-fluid";
|
|
||||||
const IMG_THUMBNAIL: &str = "img-thumbnail";
|
|
||||||
|
|
||||||
// Devuelve la clase base asociada a la imagen según la fuente.
|
|
||||||
#[inline]
|
|
||||||
fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Source::Logo(_) | Source::Responsive(_) => Self::IMG_FLUID,
|
|
||||||
Source::Thumbnail(_) => Self::IMG_THUMBNAIL,
|
|
||||||
Source::Plain(_) => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade la clase base asociada a la imagen según la fuente a la cadena de clases (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
|
||||||
let s = self.as_str();
|
|
||||||
if s.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(s);
|
|
||||||
} */
|
|
||||||
|
|
||||||
// Devuelve la clase asociada a la imagen según la fuente.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(&self) -> String {
|
|
||||||
let s = self.as_str();
|
|
||||||
if s.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
let mut class = String::with_capacity(s.len());
|
|
||||||
class.push_str(s);
|
|
||||||
class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
|
|
||||||
//!
|
|
||||||
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
|
|
||||||
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
|
|
||||||
//! desplegables [`Dropdown`](crate::theme::Dropdown).
|
|
||||||
//!
|
|
||||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
|
||||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
|
||||||
//!
|
|
||||||
//! # Ejemplo
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let nav = Nav::tabs()
|
|
||||||
//! .with_layout(nav::Layout::End)
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
|
||||||
//! .add_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Options"))
|
|
||||||
//! .with_items(TypedOp::AddMany(vec![
|
|
||||||
//! Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action")),
|
|
||||||
//! Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")),
|
|
||||||
//! ])),
|
|
||||||
//! ))
|
|
||||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Kind, Layout};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Nav;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::{Item, ItemKind};
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
|
|
||||||
///
|
|
||||||
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
|
||||||
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
|
|
||||||
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
|
||||||
///
|
|
||||||
/// Ver ejemplo en el módulo [`nav`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Nav {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
items : Children,
|
|
||||||
nav_kind : nav::Kind,
|
|
||||||
nav_layout: nav::Layout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Nav {
|
|
||||||
fn new() -> Self {
|
|
||||||
Nav::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
|
||||||
let mut classes = "nav".to_string();
|
|
||||||
self.nav_kind().push_class(&mut classes);
|
|
||||||
self.nav_layout().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
ul id=[self.id()] class=[self.classes().get()] {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Nav {
|
|
||||||
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
|
||||||
pub fn tabs() -> Self {
|
|
||||||
Nav::default().with_kind(nav::Kind::Tabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un `Nav` usando botones para los elementos (*Pills*).
|
|
||||||
pub fn pills() -> Self {
|
|
||||||
Nav::default().with_kind(nav::Kind::Pills)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
|
||||||
pub fn underline() -> Self {
|
|
||||||
Nav::default().with_kind(nav::Kind::Underline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Nav BUILDER >****************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
|
|
||||||
self.nav_kind = kind;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Selecciona la distribución y orientación del menú.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
|
||||||
self.nav_layout = layout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo elemento hijo al menú.
|
|
||||||
pub fn add_item(mut self, item: nav::Item) -> Self {
|
|
||||||
self.items.add(Child::with(item));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
|
|
||||||
self.items.alter_typed(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Nav GETTERS >****************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al menú.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el estilo visual seleccionado.
|
|
||||||
pub fn nav_kind(&self) -> &nav::Kind {
|
|
||||||
&self.nav_kind
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la distribución y orientación seleccionada.
|
|
||||||
pub fn nav_layout(&self) -> &nav::Layout {
|
|
||||||
&self.nav_layout
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la lista de elementos (`children`) del menú.
|
|
||||||
pub fn items(&self) -> &Children {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,284 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
// **< ItemKind >***********************************************************************************
|
|
||||||
|
|
||||||
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
|
|
||||||
/// [`Nav`](crate::theme::Nav).
|
|
||||||
///
|
|
||||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
|
||||||
/// con él.
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum ItemKind {
|
|
||||||
/// Elemento vacío, no produce salida.
|
|
||||||
#[default]
|
|
||||||
Void,
|
|
||||||
/// Etiqueta sin comportamiento interactivo.
|
|
||||||
Label(L10n),
|
|
||||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
|
||||||
/// inicialmente deshabilitado.
|
|
||||||
Link {
|
|
||||||
label: L10n,
|
|
||||||
path: FnPathByContext,
|
|
||||||
blank: bool,
|
|
||||||
disabled: bool,
|
|
||||||
},
|
|
||||||
/// Elemento que despliega un menú [`Dropdown`].
|
|
||||||
Dropdown(Typed<Dropdown>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemKind {
|
|
||||||
const ITEM: &str = "nav-item";
|
|
||||||
const DROPDOWN: &str = "nav-item dropdown";
|
|
||||||
|
|
||||||
// Devuelve las clases base asociadas al tipo de elemento.
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Void => "",
|
|
||||||
Self::Dropdown(_) => Self::DROPDOWN,
|
|
||||||
_ => Self::ITEM,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(&self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
} */
|
|
||||||
|
|
||||||
// Devuelve las clases asociadas al tipo de elemento.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(&self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item >***************************************************************************************
|
|
||||||
|
|
||||||
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
|
|
||||||
///
|
|
||||||
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
|
|
||||||
/// puede comportarse como texto, enlace, botón o menú desplegable según su [`ItemKind`].
|
|
||||||
///
|
|
||||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
|
||||||
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Item {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
item_kind: ItemKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Item {
|
|
||||||
fn new() -> Self {
|
|
||||||
Item::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
match self.item_kind() {
|
|
||||||
ItemKind::Void => PrepareMarkup::None,
|
|
||||||
|
|
||||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
span class="nav-link disabled" aria-disabled="true" {
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank,
|
|
||||||
disabled,
|
|
||||||
} => {
|
|
||||||
let path = path(cx);
|
|
||||||
let current_path = cx.request().map(|request| request.path());
|
|
||||||
let is_current = !*disabled && (current_path == Some(path));
|
|
||||||
|
|
||||||
let mut classes = "nav-link".to_string();
|
|
||||||
if is_current {
|
|
||||||
classes.push_str(" active");
|
|
||||||
}
|
|
||||||
if *disabled {
|
|
||||||
classes.push_str(" disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
let href = (!*disabled).then_some(path);
|
|
||||||
let target = (!*disabled && *blank).then_some("_blank");
|
|
||||||
let rel = (!*disabled && *blank).then_some("noopener noreferrer");
|
|
||||||
|
|
||||||
let aria_current = (href.is_some() && is_current).then_some("page");
|
|
||||||
let aria_disabled = (*disabled).then_some("true");
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
a
|
|
||||||
class=(classes)
|
|
||||||
href=[href]
|
|
||||||
target=[target]
|
|
||||||
rel=[rel]
|
|
||||||
aria-current=[aria_current]
|
|
||||||
aria-disabled=[aria_disabled]
|
|
||||||
{
|
|
||||||
(label.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemKind::Dropdown(menu) => {
|
|
||||||
if let Some(dd) = menu.borrow() {
|
|
||||||
let items = dd.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
|
||||||
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
|
||||||
.lookup(cx)
|
|
||||||
.unwrap_or_else(|| "Dropdown".to_string())
|
|
||||||
});
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li id=[self.id()] class=[self.classes().get()] {
|
|
||||||
a
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
href="#"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
{
|
|
||||||
(title)
|
|
||||||
}
|
|
||||||
ul class="dropdown-menu" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
PrepareMarkup::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
|
||||||
pub fn label(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Label(label),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace para la navegación.
|
|
||||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: false,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
|
||||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: false,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
|
||||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: true,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
|
||||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Link {
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
blank: true,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`].
|
|
||||||
///
|
|
||||||
/// Sólo se tienen en cuenta **el título** (si no existe le asigna uno por defecto) y **la lista
|
|
||||||
/// de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán a su
|
|
||||||
/// representación en [`Nav`].
|
|
||||||
pub fn dropdown(menu: Dropdown) -> Self {
|
|
||||||
Item {
|
|
||||||
item_kind: ItemKind::Dropdown(Typed::with(menu)),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item BUILDER >***************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del elemento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al elemento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Item GETTERS >***************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al elemento.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el tipo de elemento representado.
|
|
||||||
pub fn item_kind(&self) -> &ItemKind {
|
|
||||||
&self.item_kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,136 +0,0 @@
|
||||||
//! Definiciones para crear barras de navegación [`Navbar`].
|
|
||||||
//!
|
|
||||||
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
|
|
||||||
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
|
|
||||||
//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n).
|
|
||||||
//!
|
|
||||||
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
|
|
||||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
|
||||||
//!
|
|
||||||
//! # Ejemplos
|
|
||||||
//!
|
|
||||||
//! Barra **simple**, sólo con un menú horizontal:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let navbar = Navbar::simple()
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact"))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let navbar = Navbar::simple_toggle()
|
|
||||||
//! .with_expand(BreakPoint::MD)
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.example.com"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support"))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("PageTop"))
|
|
||||||
//! .with_path(Some(|_| "/"));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Tools"))
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Generator"), |_| "/tools/gen"))
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Reports"), |_| "/tools/reports"))
|
|
||||||
//! ))
|
|
||||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("Intranet"))
|
|
||||||
//! .with_path(Some(|_| "/"));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_right(brand)
|
|
||||||
//! .with_expand(BreakPoint::LG)
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::pills()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users"))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let oc = Offcanvas::new()
|
|
||||||
//! .with_id("main_offcanvas")
|
|
||||||
//! .with_title(L10n::n("Main menu"))
|
|
||||||
//! .with_placement(offcanvas::Placement::Start)
|
|
||||||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::offcanvas(oc)
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile"))
|
|
||||||
//! .add_item(nav::Item::dropdown(
|
|
||||||
//! Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("More"))
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings"))
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help"))
|
|
||||||
//! ))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Barra **fija arriba**:
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let brand = navbar::Brand::new()
|
|
||||||
//! .with_title(L10n::n("Main App"))
|
|
||||||
//! .with_path(Some(|_| "/"));
|
|
||||||
//!
|
|
||||||
//! let navbar = Navbar::brand_left(brand)
|
|
||||||
//! .with_position(navbar::Position::FixedTop)
|
|
||||||
//! .add_item(navbar::Item::nav(
|
|
||||||
//! Nav::new()
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors"))
|
|
||||||
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock"))
|
|
||||||
//! ));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Layout, Position};
|
|
||||||
|
|
||||||
mod brand;
|
|
||||||
pub use brand::Brand;
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Navbar;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::Item;
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
|
||||||
///
|
|
||||||
/// Representa la identidad del sitio con una imagen, título y eslogan:
|
|
||||||
///
|
|
||||||
/// - Si hay URL ([`with_path()`](Self::with_path)), el bloque completo actúa como enlace. Por
|
|
||||||
/// defecto enlaza a la raíz del sitio (`/`).
|
|
||||||
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
|
||||||
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
|
||||||
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Brand {
|
|
||||||
id : AttrId,
|
|
||||||
image : Typed<Image>,
|
|
||||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
|
||||||
title : L10n,
|
|
||||||
slogan: L10n,
|
|
||||||
#[default(_code = "Some(|_| \"/\")")]
|
|
||||||
path : Option<FnPathByContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Brand {
|
|
||||||
fn new() -> Self {
|
|
||||||
Brand::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let image = self.image().render(cx);
|
|
||||||
let title = self.title().using(cx);
|
|
||||||
if title.is_empty() && image.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
let slogan = self.slogan().using(cx);
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
@if let Some(path) = self.path() {
|
|
||||||
a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) }
|
|
||||||
} @else {
|
|
||||||
span class="navbar-brand" { (image) (title) (slogan) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Brand {
|
|
||||||
// **< Brand BUILDER >**************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_image(mut self, image: Option<Image>) -> Self {
|
|
||||||
self.image.alter_component(image);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el título de la identidad de marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_title(mut self, title: L10n) -> Self {
|
|
||||||
self.title = title;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el eslogan de la marca.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_slogan(mut self, slogan: L10n) -> Self {
|
|
||||||
self.slogan = slogan;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_path(mut self, path: Option<FnPathByContext>) -> Self {
|
|
||||||
self.path = path;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Brand GETTERS >**************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve la imagen de marca (si la hay).
|
|
||||||
pub fn image(&self) -> &Typed<Image> {
|
|
||||||
&self.image
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el título de la identidad de marca.
|
|
||||||
pub fn title(&self) -> &L10n {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el eslogan de la marca.
|
|
||||||
pub fn slogan(&self) -> &L10n {
|
|
||||||
&self.slogan
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
|
||||||
pub fn path(&self) -> &Option<FnPathByContext> {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,293 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
|
||||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
|
||||||
|
|
||||||
/// Componente para crear una **barra de navegación**.
|
|
||||||
///
|
|
||||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
|
||||||
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
|
|
||||||
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
|
|
||||||
///
|
|
||||||
/// Ver ejemplos en el módulo [`navbar`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Navbar {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
expand : BreakPoint,
|
|
||||||
layout : navbar::Layout,
|
|
||||||
position : navbar::Position,
|
|
||||||
items : Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Navbar {
|
|
||||||
fn new() -> Self {
|
|
||||||
Navbar::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
|
||||||
let mut classes = "navbar".to_string();
|
|
||||||
self.expand().push_class(&mut classes, "navbar-expand", "");
|
|
||||||
self.position().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
// Botón de despliegue (colapso u offcanvas) para la barra.
|
|
||||||
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
|
||||||
let id_content_target = join!("#", id_content);
|
|
||||||
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
|
||||||
Some("false")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
html! {
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class="navbar-toggler"
|
|
||||||
data-bs-toggle=(data_bs_toggle)
|
|
||||||
data-bs-target=(id_content_target)
|
|
||||||
aria-controls=(id_content)
|
|
||||||
aria-expanded=[aria_expanded]
|
|
||||||
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
|
|
||||||
{
|
|
||||||
span class="navbar-toggler-icon" {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas.
|
|
||||||
let id = cx.required_id::<Self>(self.id());
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
nav id=(id) class=[self.classes().get()] {
|
|
||||||
div class="container-fluid" {
|
|
||||||
@match self.layout() {
|
|
||||||
// Barra más sencilla: sólo contenido.
|
|
||||||
navbar::Layout::Simple => {
|
|
||||||
(items)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra sencilla que se puede contraer/expandir.
|
|
||||||
navbar::Layout::SimpleToggle => {
|
|
||||||
@let id_content = join!(id, "-content");
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
div id=(id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda, siempre visible.
|
|
||||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
|
||||||
(brand.render(cx))
|
|
||||||
(items)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda y botón a la derecha.
|
|
||||||
navbar::Layout::BrandLeft(brand) => {
|
|
||||||
@let id_content = join!(id, "-content");
|
|
||||||
|
|
||||||
(brand.render(cx))
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
div id=(id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con botón a la izquierda y marca a la derecha.
|
|
||||||
navbar::Layout::BrandRight(brand) => {
|
|
||||||
@let id_content = join!(id, "-content");
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
|
||||||
(brand.render(cx))
|
|
||||||
div id=(id_content) class="collapse navbar-collapse" {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
|
|
||||||
navbar::Layout::Offcanvas(offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con marca a la izquierda y contenido en offcanvas.
|
|
||||||
navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(brand.render(cx))
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Barra con contenido en offcanvas y marca a la derecha.
|
|
||||||
navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
|
|
||||||
@let id_content = offcanvas.id().unwrap_or_default();
|
|
||||||
|
|
||||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
|
||||||
(brand.render(cx))
|
|
||||||
@if let Some(oc) = offcanvas.borrow() {
|
|
||||||
(oc.render_offcanvas(cx, Some(self.items())))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Navbar {
|
|
||||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
|
||||||
pub fn simple() -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::Simple)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
|
|
||||||
pub fn simple_toggle() -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::SimpleToggle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
|
||||||
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
|
||||||
pub fn brand_left(brand: navbar::Brand) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
|
||||||
pub fn brand_right(brand: navbar::Brand) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
|
||||||
pub fn offcanvas(oc: Offcanvas) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
|
||||||
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
|
||||||
Typed::with(brand),
|
|
||||||
Typed::with(oc),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
|
||||||
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
|
||||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
|
||||||
Typed::with(brand),
|
|
||||||
Typed::with(oc),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Navbar BUILDER >*************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) de la barra de navegación.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas a la barra de navegación.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Modificar el color de fondo ([`classes::Background`]).
|
|
||||||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
|
|
||||||
self.expand = bp;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el tipo de disposición que tendrá la barra de navegación.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
|
|
||||||
self.layout = layout;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define dónde se mostrará la barra de navegación dentro del documento.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_position(mut self, position: navbar::Position) -> Self {
|
|
||||||
self.position = position;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo contenido hijo.
|
|
||||||
#[inline]
|
|
||||||
pub fn add_item(mut self, item: navbar::Item) -> Self {
|
|
||||||
self.items.add(Child::with(item));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
|
||||||
self.items.alter_typed(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Navbar GETTERS >*************************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el punto de ruptura configurado.
|
|
||||||
pub fn expand(&self) -> &BreakPoint {
|
|
||||||
&self.expand
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la disposición configurada para la barra de navegación.
|
|
||||||
pub fn layout(&self) -> &navbar::Layout {
|
|
||||||
&self.layout
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la posición configurada para la barra de navegación.
|
|
||||||
pub fn position(&self) -> &navbar::Position {
|
|
||||||
&self.position
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la lista de contenidos (`children`).
|
|
||||||
pub fn items(&self) -> &Children {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
|
||||||
///
|
|
||||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
|
||||||
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
|
||||||
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum Item {
|
|
||||||
/// Sin contenido, no produce salida.
|
|
||||||
#[default]
|
|
||||||
Void,
|
|
||||||
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
|
|
||||||
///
|
|
||||||
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
|
||||||
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
|
||||||
/// sólo añade otra dentro del bloque de contenidos.
|
|
||||||
Brand(Typed<navbar::Brand>),
|
|
||||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
|
||||||
Nav(Typed<Nav>),
|
|
||||||
/// Representa un texto libre localizado.
|
|
||||||
Text(L10n),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Item {
|
|
||||||
fn new() -> Self {
|
|
||||||
Item::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
Self::Void => None,
|
|
||||||
Self::Brand(brand) => brand.id(),
|
|
||||||
Self::Nav(nav) => nav.id(),
|
|
||||||
Self::Text(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
if let Self::Nav(nav) = self {
|
|
||||||
if let Some(mut nav) = nav.borrow_mut() {
|
|
||||||
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
match self {
|
|
||||||
Self::Void => PrepareMarkup::None,
|
|
||||||
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
|
|
||||||
Self::Nav(nav) => {
|
|
||||||
if let Some(nav) = nav.borrow() {
|
|
||||||
let items = nav.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
PrepareMarkup::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Text(text) => PrepareMarkup::With(html! {
|
|
||||||
span class="navbar-text" {
|
|
||||||
(text.using(cx))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Crea un elemento de tipo [`navbar::Brand`] para añadir en el contenido de [`Navbar`].
|
|
||||||
///
|
|
||||||
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
|
||||||
/// desplegable.
|
|
||||||
pub fn brand(brand: navbar::Brand) -> Self {
|
|
||||||
Self::Brand(Typed::with(brand))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
|
||||||
pub fn nav(item: Nav) -> Self {
|
|
||||||
Self::Nav(Typed::with(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un elemento de texto localizado, mostrado sin interacción.
|
|
||||||
pub fn text(item: L10n) -> Self {
|
|
||||||
Self::Text(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
// **< Layout >*************************************************************************************
|
|
||||||
|
|
||||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum Layout {
|
|
||||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
|
||||||
///
|
|
||||||
/// La barra de navegación no se colapsa.
|
|
||||||
#[default]
|
|
||||||
Simple,
|
|
||||||
|
|
||||||
/// Barra simple, con botón de despliegue a la izquierda y sin marca de identidad.
|
|
||||||
SimpleToggle,
|
|
||||||
|
|
||||||
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
|
||||||
///
|
|
||||||
/// La barra de navegación no se colapsa.
|
|
||||||
SimpleBrandLeft(Typed<navbar::Brand>),
|
|
||||||
|
|
||||||
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
|
||||||
BrandLeft(Typed<navbar::Brand>),
|
|
||||||
|
|
||||||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
|
||||||
BrandRight(Typed<navbar::Brand>),
|
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
|
||||||
Offcanvas(Typed<Offcanvas>),
|
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
|
||||||
/// derecha.
|
|
||||||
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
|
|
||||||
|
|
||||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
|
||||||
/// derecha.
|
|
||||||
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Position >***********************************************************************************
|
|
||||||
|
|
||||||
/// Posición global de una barra de navegación [`Navbar`] en el documento.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Position {
|
|
||||||
/// Barra normal, fluye con el documento.
|
|
||||||
#[default]
|
|
||||||
Static,
|
|
||||||
/// Barra fijada en la parte superior, siempre visible.
|
|
||||||
///
|
|
||||||
/// Puede ser necesario reservar espacio en la parte superior del contenido que fluye debajo
|
|
||||||
/// para evitar que quede oculto por la barra.
|
|
||||||
FixedTop,
|
|
||||||
/// Barra fijada en la parte inferior, siempre visible.
|
|
||||||
///
|
|
||||||
/// Puede ser necesario reservar espacio en la parte inferior del contenido que fluye debajo
|
|
||||||
/// para evitar que quede oculto por la barra.
|
|
||||||
FixedBottom,
|
|
||||||
/// La barra de navegación se fija en la parte superior al hacer *scroll*.
|
|
||||||
StickyTop,
|
|
||||||
/// La barra de navegación se fija en la parte inferior al hacer *scroll*.
|
|
||||||
StickyBottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Position {
|
|
||||||
// Devuelve la clase base asociada a la posición de la barra de navegación.
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Static => "",
|
|
||||||
Self::FixedTop => "fixed-top",
|
|
||||||
Self::FixedBottom => "fixed-bottom",
|
|
||||||
Self::StickyTop => "sticky-top",
|
|
||||||
Self::StickyBottom => "sticky-bottom",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
|
|
||||||
// aplica (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
self.as_str().to_string()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
|
|
||||||
//!
|
|
||||||
//! # Ejemplo
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use pagetop::prelude::*;
|
|
||||||
//! # use pagetop_bootsier::prelude::*;
|
|
||||||
//! let panel = Offcanvas::new()
|
|
||||||
//! .with_id("offcanvas_example")
|
|
||||||
//! .with_title(L10n::n("Offcanvas title"))
|
|
||||||
//! .with_placement(offcanvas::Placement::End)
|
|
||||||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
|
||||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
|
||||||
//! .with_visibility(offcanvas::Visibility::Default)
|
|
||||||
//! .add_child(Dropdown::new()
|
|
||||||
//! .with_title(L10n::n("Menu"))
|
|
||||||
//! .add_item(dropdown::Item::label(L10n::n("Label")))
|
|
||||||
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
|
|
||||||
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
|
|
||||||
//! );
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
mod props;
|
|
||||||
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
|
||||||
|
|
||||||
mod component;
|
|
||||||
pub use component::Offcanvas;
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::LOCALES_BOOTSIER;
|
|
||||||
|
|
||||||
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales.
|
|
||||||
///
|
|
||||||
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
|
||||||
/// características principales:
|
|
||||||
///
|
|
||||||
/// - Puede mostrar una capa de fondo para centrar la atención del usuario en el panel
|
|
||||||
/// ([`with_backdrop()`](Self::with_backdrop)); o puede bloquear el desplazamiento del documento
|
|
||||||
/// principal ([`with_body_scroll()`](Self::with_body_scroll)).
|
|
||||||
/// - Se puede configurar el borde de la ventana desde el que se desliza el panel
|
|
||||||
/// ([`with_placement()`](Self::with_placement)).
|
|
||||||
/// - Encabezado con título ([`with_title()`](Self::with_title)) y **botón de cierre** integrado.
|
|
||||||
/// - Puede cambiar su comportamiento a partir de un punto de ruptura
|
|
||||||
/// ([`with_breakpoint()`](Self::with_breakpoint)).
|
|
||||||
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
|
|
||||||
/// adecuados para lectores de pantalla y navegación por teclado.
|
|
||||||
///
|
|
||||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Offcanvas {
|
|
||||||
id : AttrId,
|
|
||||||
classes : AttrClasses,
|
|
||||||
title : L10n,
|
|
||||||
breakpoint: BreakPoint,
|
|
||||||
backdrop : offcanvas::Backdrop,
|
|
||||||
scrolling : offcanvas::BodyScroll,
|
|
||||||
placement : offcanvas::Placement,
|
|
||||||
visibility: offcanvas::Visibility,
|
|
||||||
children : Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Offcanvas {
|
|
||||||
fn new() -> Self {
|
|
||||||
Offcanvas::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, {
|
|
||||||
let mut classes = "offcanvas".to_string();
|
|
||||||
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
|
||||||
self.placement().push_class(&mut classes);
|
|
||||||
self.visibility().push_class(&mut classes);
|
|
||||||
classes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
PrepareMarkup::With(self.render_offcanvas(cx, None))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Offcanvas {
|
|
||||||
// **< Offcanvas BUILDER >**********************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único (`id`) del panel.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de clases CSS aplicadas al panel.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el título del encabezado.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_title(mut self, title: L10n) -> Self {
|
|
||||||
self.title = title;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el punto de ruptura a partir del cual cambia el comportamiento del panel.
|
|
||||||
///
|
|
||||||
/// - **Por debajo** de ese tamaño de pantalla, el componente actúa como panel deslizante
|
|
||||||
/// ([`Offcanvas`]).
|
|
||||||
/// - **Por encima**, el contenido del panel se muestra tal cual, integrado en la página.
|
|
||||||
///
|
|
||||||
/// Por ejemplo, con `BreakPoint::LG`, será *offcanvas* en móviles y tabletas, y visible
|
|
||||||
/// directamente en pantallas grandes. Por defecto usa `BreakPoint::None` para que sea
|
|
||||||
/// *offcanvas* siempre.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_breakpoint(mut self, bp: BreakPoint) -> Self {
|
|
||||||
self.breakpoint = bp;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ajusta la capa de fondo del panel para definir su comportamiento al hacer clic fuera del
|
|
||||||
/// panel.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_backdrop(mut self, backdrop: offcanvas::Backdrop) -> Self {
|
|
||||||
self.backdrop = backdrop;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self {
|
|
||||||
self.scrolling = scrolling;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indica desde qué borde de la ventana entra y se ancla el panel.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_placement(mut self, placement: offcanvas::Placement) -> Self {
|
|
||||||
self.placement = placement;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fija el estado inicial del panel (oculto o visible al cargar).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_visibility(mut self, visibility: offcanvas::Visibility) -> Self {
|
|
||||||
self.visibility = visibility;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo componente hijo al panel.
|
|
||||||
#[inline]
|
|
||||||
pub fn add_child(mut self, child: impl Component) -> Self {
|
|
||||||
self.children.add(Child::with(child));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_children(mut self, op: ChildOp) -> Self {
|
|
||||||
self.children.alter_child(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Offcanvas GETTERS >**********************************************************************
|
|
||||||
|
|
||||||
/// Devuelve las clases CSS asociadas al panel.
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el título del panel.
|
|
||||||
pub fn title(&self) -> &L10n {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
|
||||||
pub fn breakpoint(&self) -> &BreakPoint {
|
|
||||||
&self.breakpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
|
||||||
pub fn backdrop(&self) -> &offcanvas::Backdrop {
|
|
||||||
&self.backdrop
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
|
||||||
pub fn body_scroll(&self) -> &offcanvas::BodyScroll {
|
|
||||||
&self.scrolling
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la posición de inicio del panel.
|
|
||||||
pub fn placement(&self) -> &offcanvas::Placement {
|
|
||||||
&self.placement
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve el estado inicial del panel.
|
|
||||||
pub fn visibility(&self) -> &offcanvas::Visibility {
|
|
||||||
&self.visibility
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la lista de componentes (`children`) del panel.
|
|
||||||
pub fn children(&self) -> &Children {
|
|
||||||
&self.children
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Offcanvas HELPERS >**********************************************************************
|
|
||||||
|
|
||||||
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
|
|
||||||
let body = self.children().render(cx);
|
|
||||||
let body_extra = extra.map(|c| c.render(cx)).unwrap_or_else(|| html! {});
|
|
||||||
if body.is_empty() && body_extra.is_empty() {
|
|
||||||
return html! {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = cx.required_id::<Self>(self.id());
|
|
||||||
let id_label = join!(id, "-label");
|
|
||||||
let id_target = join!("#", id);
|
|
||||||
|
|
||||||
let body_scroll = match self.body_scroll() {
|
|
||||||
offcanvas::BodyScroll::Disabled => None,
|
|
||||||
offcanvas::BodyScroll::Enabled => Some("true"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let backdrop = match self.backdrop() {
|
|
||||||
offcanvas::Backdrop::Disabled => Some("false"),
|
|
||||||
offcanvas::Backdrop::Enabled => None,
|
|
||||||
offcanvas::Backdrop::Static => Some("static"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let title = self.title().using(cx);
|
|
||||||
|
|
||||||
html! {
|
|
||||||
div
|
|
||||||
id=(id)
|
|
||||||
class=[self.classes().get()]
|
|
||||||
tabindex="-1"
|
|
||||||
data-bs-scroll=[body_scroll]
|
|
||||||
data-bs-backdrop=[backdrop]
|
|
||||||
aria-labelledby=(id_label)
|
|
||||||
{
|
|
||||||
div class="offcanvas-header" {
|
|
||||||
@if !title.is_empty() {
|
|
||||||
h5 class="offcanvas-title" id=(id_label) { (title) }
|
|
||||||
}
|
|
||||||
button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="offcanvas"
|
|
||||||
data-bs-target=(id_target)
|
|
||||||
aria-label=[L10n::t("offcanvas_close", &LOCALES_BOOTSIER).lookup(cx)]
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
div class="offcanvas-body" {
|
|
||||||
(body)
|
|
||||||
(body_extra)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
// **< Backdrop >***********************************************************************************
|
|
||||||
|
|
||||||
/// Comportamiento de la capa de fondo (*backdrop*) de un panel
|
|
||||||
/// [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Backdrop {
|
|
||||||
/// Sin capa de fondo, la página principal permanece visible e interactiva.
|
|
||||||
Disabled,
|
|
||||||
/// Opción por defecto, se oscurece el fondo; un clic fuera del panel suele cerrarlo.
|
|
||||||
#[default]
|
|
||||||
Enabled,
|
|
||||||
/// Muestra la capa de fondo pero no se cierra al hacer clic fuera del panel. Útil si se
|
|
||||||
/// requiere completar una acción antes de salir.
|
|
||||||
Static,
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< BodyScroll >*********************************************************************************
|
|
||||||
|
|
||||||
/// Controla si la página principal puede desplazarse al abrir un panel
|
|
||||||
/// [`Offcanvas`](crate::theme::Offcanvas).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum BodyScroll {
|
|
||||||
/// Opción por defecto, la página principal se bloquea centrando la interacción en el panel.
|
|
||||||
#[default]
|
|
||||||
Disabled,
|
|
||||||
/// Permite el desplazamiento de la página principal.
|
|
||||||
Enabled,
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Placement >**********************************************************************************
|
|
||||||
|
|
||||||
/// Posición de aparición de un panel [`Offcanvas`](crate::theme::Offcanvas) al deslizarse.
|
|
||||||
///
|
|
||||||
/// Define desde qué borde de la ventana entra y se ancla el panel.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Placement {
|
|
||||||
/// Opción por defecto, desde el borde inicial según dirección de lectura (respetando LTR/RTL).
|
|
||||||
#[default]
|
|
||||||
Start,
|
|
||||||
/// Desde el borde final según dirección de lectura (respetando LTR/RTL).
|
|
||||||
End,
|
|
||||||
/// Desde la parte superior.
|
|
||||||
Top,
|
|
||||||
/// Desde la parte inferior.
|
|
||||||
Bottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Placement {
|
|
||||||
// Devuelve la clase base asociada a la posición de aparición del panel.
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Placement::Start => "offcanvas-start",
|
|
||||||
Placement::End => "offcanvas-end",
|
|
||||||
Placement::Top => "offcanvas-top",
|
|
||||||
Placement::Bottom => "offcanvas-bottom",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(self.as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase asociada a la posición de aparición del panel (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Visibility >*********************************************************************************
|
|
||||||
|
|
||||||
/// Estado inicial de un panel [`Offcanvas`](crate::theme::Offcanvas).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Visibility {
|
|
||||||
/// El panel permanece oculto desde el principio.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// El panel se muestra abierto al cargar.
|
|
||||||
Show,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Visibility {
|
|
||||||
// Devuelve la clase base asociada al estado inicial del panel.
|
|
||||||
#[inline]
|
|
||||||
const fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Visibility::Default => "",
|
|
||||||
Visibility::Show => "show",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn push_class(self, classes: &mut String) {
|
|
||||||
let class = self.as_str();
|
|
||||||
if class.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !classes.is_empty() {
|
|
||||||
classes.push(' ');
|
|
||||||
}
|
|
||||||
classes.push_str(class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve la clase asociada al estado inicial, o una cadena vacía si no aplica (reservado).
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn to_class(self) -> String {
|
|
||||||
self.as_str().to_owned()
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
/*!
|
/*!
|
||||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
* Bootstrap v5.3.3 (https://getbootstrap.com/)
|
||||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
* Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
|
|
@ -224,7 +224,7 @@
|
||||||
* @param {HTMLElement} element
|
* @param {HTMLElement} element
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
* @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
|
||||||
*/
|
*/
|
||||||
const reflow = element => {
|
const reflow = element => {
|
||||||
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
element.offsetHeight; // eslint-disable-line no-unused-expressions
|
||||||
|
|
@ -269,7 +269,7 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {
|
||||||
return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue;
|
return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;
|
||||||
};
|
};
|
||||||
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
|
||||||
if (!waitForTransition) {
|
if (!waitForTransition) {
|
||||||
|
|
@ -591,7 +591,7 @@
|
||||||
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));
|
||||||
for (const key of bsKeys) {
|
for (const key of bsKeys) {
|
||||||
let pureKey = key.replace(/^bs/, '');
|
let pureKey = key.replace(/^bs/, '');
|
||||||
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1);
|
pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);
|
||||||
attributes[pureKey] = normalizeData(element.dataset[key]);
|
attributes[pureKey] = normalizeData(element.dataset[key]);
|
||||||
}
|
}
|
||||||
return attributes;
|
return attributes;
|
||||||
|
|
@ -666,7 +666,7 @@
|
||||||
* Constants
|
* Constants
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = '5.3.8';
|
const VERSION = '5.3.3';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class definition
|
* Class definition
|
||||||
|
|
@ -692,8 +692,6 @@
|
||||||
this[propertyName] = null;
|
this[propertyName] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
|
||||||
_queueCallback(callback, element, isAnimated = true) {
|
_queueCallback(callback, element, isAnimated = true) {
|
||||||
executeAfterTransition(callback, element, isAnimated);
|
executeAfterTransition(callback, element, isAnimated);
|
||||||
}
|
}
|
||||||
|
|
@ -1625,11 +1623,11 @@
|
||||||
this._element.style[dimension] = '';
|
this._element.style[dimension] = '';
|
||||||
this._queueCallback(complete, this._element, true);
|
this._queueCallback(complete, this._element, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
|
||||||
_isShown(element = this._element) {
|
_isShown(element = this._element) {
|
||||||
return element.classList.contains(CLASS_NAME_SHOW$7);
|
return element.classList.contains(CLASS_NAME_SHOW$7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
_configAfterMerge(config) {
|
_configAfterMerge(config) {
|
||||||
config.toggle = Boolean(config.toggle); // Coerce string values
|
config.toggle = Boolean(config.toggle); // Coerce string values
|
||||||
config.parent = getElement(config.parent);
|
config.parent = getElement(config.parent);
|
||||||
|
|
@ -1883,7 +1881,7 @@
|
||||||
}
|
}
|
||||||
_createPopper() {
|
_createPopper() {
|
||||||
if (typeof Popper__namespace === 'undefined') {
|
if (typeof Popper__namespace === 'undefined') {
|
||||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)');
|
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)');
|
||||||
}
|
}
|
||||||
let referenceElement = this._element;
|
let referenceElement = this._element;
|
||||||
if (this._config.reference === 'parent') {
|
if (this._config.reference === 'parent') {
|
||||||
|
|
@ -1962,7 +1960,7 @@
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...defaultBsPopperConfig,
|
...defaultBsPopperConfig,
|
||||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_selectMenuItem({
|
_selectMenuItem({
|
||||||
|
|
@ -2984,6 +2982,7 @@
|
||||||
*
|
*
|
||||||
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
* Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line unicorn/better-regex
|
||||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;
|
||||||
const allowedAttribute = (attribute, allowedAttributeList) => {
|
const allowedAttribute = (attribute, allowedAttributeList) => {
|
||||||
const attributeName = attribute.nodeName.toLowerCase();
|
const attributeName = attribute.nodeName.toLowerCase();
|
||||||
|
|
@ -3148,7 +3147,7 @@
|
||||||
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;
|
||||||
}
|
}
|
||||||
_resolvePossibleFunction(arg) {
|
_resolvePossibleFunction(arg) {
|
||||||
return execute(arg, [undefined, this]);
|
return execute(arg, [this]);
|
||||||
}
|
}
|
||||||
_putElementInTemplate(element, templateElement) {
|
_putElementInTemplate(element, templateElement) {
|
||||||
if (this._config.html) {
|
if (this._config.html) {
|
||||||
|
|
@ -3247,7 +3246,7 @@
|
||||||
class Tooltip extends BaseComponent {
|
class Tooltip extends BaseComponent {
|
||||||
constructor(element, config) {
|
constructor(element, config) {
|
||||||
if (typeof Popper__namespace === 'undefined') {
|
if (typeof Popper__namespace === 'undefined') {
|
||||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)');
|
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org)');
|
||||||
}
|
}
|
||||||
super(element, config);
|
super(element, config);
|
||||||
|
|
||||||
|
|
@ -3293,6 +3292,7 @@
|
||||||
if (!this._isEnabled) {
|
if (!this._isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._activeTrigger.click = !this._activeTrigger.click;
|
||||||
if (this._isShown()) {
|
if (this._isShown()) {
|
||||||
this._leave();
|
this._leave();
|
||||||
return;
|
return;
|
||||||
|
|
@ -3480,7 +3480,7 @@
|
||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
_resolvePossibleFunction(arg) {
|
_resolvePossibleFunction(arg) {
|
||||||
return execute(arg, [this._element, this._element]);
|
return execute(arg, [this._element]);
|
||||||
}
|
}
|
||||||
_getPopperConfig(attachment) {
|
_getPopperConfig(attachment) {
|
||||||
const defaultBsPopperConfig = {
|
const defaultBsPopperConfig = {
|
||||||
|
|
@ -3518,7 +3518,7 @@
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...defaultBsPopperConfig,
|
...defaultBsPopperConfig,
|
||||||
...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
...execute(this._config.popperConfig, [defaultBsPopperConfig])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_setListeners() {
|
_setListeners() {
|
||||||
|
|
@ -3527,7 +3527,6 @@
|
||||||
if (trigger === 'click') {
|
if (trigger === 'click') {
|
||||||
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {
|
||||||
const context = this._initializeOnDelegatedTarget(event);
|
const context = this._initializeOnDelegatedTarget(event);
|
||||||
context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]);
|
|
||||||
context.toggle();
|
context.toggle();
|
||||||
});
|
});
|
||||||
} else if (trigger !== TRIGGER_MANUAL) {
|
} else if (trigger !== TRIGGER_MANUAL) {
|
||||||
|
|
@ -4393,6 +4392,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
_maybeScheduleHide() {
|
_maybeScheduleHide() {
|
||||||
if (!this._config.autohide) {
|
if (!this._config.autohide) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
7
extensions/pagetop-bootsier/static/js/bootstrap.min.js
vendored
Normal file
7
extensions/pagetop-bootsier/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/pagetop-bootsier/static/js/bootstrap.min.js.map
vendored
Normal file
1
extensions/pagetop-bootsier/static/js/bootstrap.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2,22 +2,15 @@
|
||||||
$enable-grid-classes: false;
|
$enable-grid-classes: false;
|
||||||
$enable-cssgrid: true;
|
$enable-cssgrid: true;
|
||||||
|
|
||||||
// Opacity
|
// Text opacity
|
||||||
.bg-opacity-0 {
|
|
||||||
--bs-bg-opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-opacity-0 {
|
|
||||||
--bs-border-opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-opacity-0 {
|
|
||||||
--bs-text-opacity: 0;
|
|
||||||
}
|
|
||||||
.text-opacity-10 {
|
.text-opacity-10 {
|
||||||
--bs-text-opacity: 0.1;
|
--bs-text-opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Extending utilities
|
// Extending utilities
|
||||||
$utilities: map-merge(
|
$utilities: map-merge(
|
||||||
$utilities,
|
$utilities,
|
||||||
|
|
@ -1,55 +1,54 @@
|
||||||
@import "bootstrap-5.3.8/mixins/banner";
|
@import "bootstrap-5.3.3/mixins/banner";
|
||||||
@include bsBanner("");
|
@include bsBanner("");
|
||||||
|
|
||||||
|
|
||||||
// scss-docs-start import-stack
|
// scss-docs-start import-stack
|
||||||
// Configuration
|
// Configuration
|
||||||
@import "bootstrap-5.3.8/functions";
|
@import "bootstrap-5.3.3/functions";
|
||||||
@import "bootstrap-5.3.8/variables";
|
@import "bootstrap-5.3.3/variables";
|
||||||
@import "bootstrap-5.3.8/variables-dark";
|
@import "bootstrap-5.3.3/variables-dark";
|
||||||
@import "bootstrap-5.3.8/maps";
|
@import "bootstrap-5.3.3/maps";
|
||||||
@import "bootstrap-5.3.8/mixins";
|
@import "bootstrap-5.3.3/mixins";
|
||||||
@import "bootstrap-5.3.8/utilities";
|
@import "bootstrap-5.3.3/utilities";
|
||||||
|
|
||||||
|
@import "custom";
|
||||||
|
|
||||||
// Layout & components
|
// Layout & components
|
||||||
@import "bootstrap-5.3.8/root";
|
@import "bootstrap-5.3.3/root";
|
||||||
@import "bootstrap-5.3.8/reboot";
|
@import "bootstrap-5.3.3/reboot";
|
||||||
@import "bootstrap-5.3.8/type";
|
@import "bootstrap-5.3.3/type";
|
||||||
@import "bootstrap-5.3.8/images";
|
@import "bootstrap-5.3.3/images";
|
||||||
@import "bootstrap-5.3.8/containers";
|
@import "bootstrap-5.3.3/containers";
|
||||||
@import "bootstrap-5.3.8/grid";
|
@import "bootstrap-5.3.3/grid";
|
||||||
@import "bootstrap-5.3.8/tables";
|
@import "bootstrap-5.3.3/tables";
|
||||||
@import "bootstrap-5.3.8/forms";
|
@import "bootstrap-5.3.3/forms";
|
||||||
@import "bootstrap-5.3.8/buttons";
|
@import "bootstrap-5.3.3/buttons";
|
||||||
@import "bootstrap-5.3.8/transitions";
|
@import "bootstrap-5.3.3/transitions";
|
||||||
@import "bootstrap-5.3.8/dropdown";
|
@import "bootstrap-5.3.3/dropdown";
|
||||||
@import "bootstrap-5.3.8/button-group";
|
@import "bootstrap-5.3.3/button-group";
|
||||||
@import "bootstrap-5.3.8/nav";
|
@import "bootstrap-5.3.3/nav";
|
||||||
@import "bootstrap-5.3.8/navbar";
|
@import "bootstrap-5.3.3/navbar";
|
||||||
@import "bootstrap-5.3.8/card";
|
@import "bootstrap-5.3.3/card";
|
||||||
@import "bootstrap-5.3.8/accordion";
|
@import "bootstrap-5.3.3/accordion";
|
||||||
@import "bootstrap-5.3.8/breadcrumb";
|
@import "bootstrap-5.3.3/breadcrumb";
|
||||||
@import "bootstrap-5.3.8/pagination";
|
@import "bootstrap-5.3.3/pagination";
|
||||||
@import "bootstrap-5.3.8/badge";
|
@import "bootstrap-5.3.3/badge";
|
||||||
@import "bootstrap-5.3.8/alert";
|
@import "bootstrap-5.3.3/alert";
|
||||||
@import "bootstrap-5.3.8/progress";
|
@import "bootstrap-5.3.3/progress";
|
||||||
@import "bootstrap-5.3.8/list-group";
|
@import "bootstrap-5.3.3/list-group";
|
||||||
@import "bootstrap-5.3.8/close";
|
@import "bootstrap-5.3.3/close";
|
||||||
@import "bootstrap-5.3.8/toasts";
|
@import "bootstrap-5.3.3/toasts";
|
||||||
@import "bootstrap-5.3.8/modal";
|
@import "bootstrap-5.3.3/modal";
|
||||||
@import "bootstrap-5.3.8/tooltip";
|
@import "bootstrap-5.3.3/tooltip";
|
||||||
@import "bootstrap-5.3.8/popover";
|
@import "bootstrap-5.3.3/popover";
|
||||||
@import "bootstrap-5.3.8/carousel";
|
@import "bootstrap-5.3.3/carousel";
|
||||||
@import "bootstrap-5.3.8/spinners";
|
@import "bootstrap-5.3.3/spinners";
|
||||||
@import "bootstrap-5.3.8/offcanvas";
|
@import "bootstrap-5.3.3/offcanvas";
|
||||||
@import "bootstrap-5.3.8/placeholders";
|
@import "bootstrap-5.3.3/placeholders";
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
@import "bootstrap-5.3.8/helpers";
|
@import "bootstrap-5.3.3/helpers";
|
||||||
|
|
||||||
// Custom definitions
|
|
||||||
@import "customs";
|
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
@import "bootstrap-5.3.8/utilities/api";
|
@import "bootstrap-5.3.3/utilities/api";
|
||||||
// scss-docs-end import-stack
|
// scss-docs-end import-stack
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,17 @@
|
||||||
&:last-child { border-bottom: 0; }
|
&:last-child { border-bottom: 0; }
|
||||||
|
|
||||||
// stylelint-disable selector-max-class
|
// stylelint-disable selector-max-class
|
||||||
> .accordion-collapse,
|
> .accordion-header .accordion-button {
|
||||||
> .accordion-header .accordion-button,
|
&,
|
||||||
> .accordion-header .accordion-button.collapsed {
|
&.collapsed {
|
||||||
@include border-radius(0);
|
@include border-radius(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// stylelint-enable selector-max-class
|
// stylelint-enable selector-max-class
|
||||||
|
|
||||||
|
> .accordion-collapse {
|
||||||
|
@include border-radius(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue