Compare commits
No commits in common. "legacy/better-packs" and "main" have entirely different histories.
legacy/bet
...
main
543 changed files with 40140 additions and 17709 deletions
69
.cargo/cliff.toml
Normal file
69
.cargo/cliff.toml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# 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" },
|
||||
]
|
||||
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[alias]
|
||||
ts = ["test", "--features", "testing"] # cargo ts
|
||||
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
||||
25
.cargo/release.toml
Normal file
25
.cargo/release.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 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"
|
||||
]
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,4 +1,10 @@
|
|||
# Ignora directorios de compilación
|
||||
**/target
|
||||
|
||||
# Archivos de log
|
||||
**/log/*.log*
|
||||
Cargo.lock
|
||||
workdir
|
||||
|
||||
# Archivos de configuración locales
|
||||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
|
|
|
|||
86
CHANGELOG.md
Normal file
86
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# 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
|
||||
57
CREDITS.md
57
CREDITS.md
|
|
@ -1,46 +1,33 @@
|
|||
# ⌨️ Código
|
||||
# 🔃 Dependencias
|
||||
|
||||
* PageTop incluye código de la versión [0.11.0](https://github.com/mehcode/config-rs/tree/0.11.0) de
|
||||
[config](https://crates.io/crates/config), de [Ryan Leckey](https://crates.io/users/mehcode), por
|
||||
las facilidades que ofrece frente a sus versiones más modernas para leer los ajustes de
|
||||
configuración y delegar su asignación a tipos seguros según los requerimientos de cada módulo,
|
||||
tema o aplicación.
|
||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
|
||||
|
||||
* PageTop incorpora una versión adaptada del excelente *crate* [maud](https://crates.io/crates/maud)
|
||||
de [Chris Wong](https://crates.io/users/lambda-fairy) (versión
|
||||
[0.25.0](https://github.com/lambda-fairy/maud/tree/v0.25.0/maud)), para añadir sus funcionalidades
|
||||
sin requerir la referencia a `maud` en el archivo `Cargo.toml` de cada proyecto.
|
||||
|
||||
* PageTop usa los reconocidos *crates* [SQLx](https://github.com/launchbadge/sqlx) y
|
||||
[SeaQuery](https://github.com/SeaQL/sea-query) para interactuar con bases de datos. Sin embargo,
|
||||
para restringir las migraciones a módulos, se ha integrado en el código una versión modificada de
|
||||
[SeaORM Migration](https://github.com/SeaQL/sea-orm/tree/master/sea-orm-migration) (versión
|
||||
[0.11.3](https://github.com/SeaQL/sea-orm/tree/0.11.3/sea-orm-migration/src)).
|
||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
||||
de la aplicación.
|
||||
* [Fluent templates](https://github.com/XAMPPRocky/fluent-templates), que integra
|
||||
[Fluent](https://projectfluent.org/) para internacionalizar las aplicaciones.
|
||||
* Además de otros *crates* adicionales que se pueden explorar en los archivos `Cargo.toml` de
|
||||
PageTop y sus extensiones.
|
||||
|
||||
|
||||
# 🗚 FIGfonts
|
||||
|
||||
PageTop utiliza el paquete [figlet-rs](https://crates.io/crates/figlet-rs) de *yuanbohan* para
|
||||
mostrar en el terminal un rótulo de presentación con el nombre de la aplicación usando caracteres
|
||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `src/app` son:
|
||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
||||
[FIGlet](http://www.figlet.org). Las fuentes incluidas en `pagetop/src/app` son:
|
||||
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) por *Glenn Chappell*.
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) por *Glenn Chappell*
|
||||
(predeterminado).
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) por *Claude Martins*.
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) por *Ryan Youck*.
|
||||
|
||||
|
||||
# 📰 Plantillas
|
||||
|
||||
* El diseño de la página predeterminada de inicio está basado en la plantilla
|
||||
[Zinc](https://themewagon.com/themes/free-bootstrap-5-html5-business-website-template-zinc) creada
|
||||
por [inovatik](https://inovatik.com/) y distribuida por [ThemeWagon](https://themewagon.com).
|
||||
* [slant.flf](http://www.figlet.org/fontdb_example.cgi?font=slant.flf) de *Glenn Chappell*
|
||||
* [small.flf](http://www.figlet.org/fontdb_example.cgi?font=small.flf) de *Glenn Chappell*
|
||||
(predeterminada)
|
||||
* [speed.flf](http://www.figlet.org/fontdb_example.cgi?font=speed.flf) de *Claude Martins*
|
||||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
|
||||
|
||||
# 🎨 Icono
|
||||
|
||||
El monstruo sonriente de Frankenstein es una divertida creación de
|
||||
[Webalys](https://www.iconfinder.com/webalys). Puede encontrarse en su colección
|
||||
[Nasty Icons](https://www.iconfinder.com/iconsets/nasty) disponible en
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
|
|
|
|||
3123
Cargo.lock
generated
Normal file
3123
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
109
Cargo.toml
109
Cargo.toml
|
|
@ -1,27 +1,90 @@
|
|||
[package]
|
||||
name = "pagetop"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
|
||||
"""
|
||||
categories = ["web-programming::http-server"]
|
||||
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
colored = "3.0"
|
||||
concat-string = "1.0"
|
||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||
figlet-rs = "0.1"
|
||||
indoc = "2.0"
|
||||
itoa = "1.0"
|
||||
parking_lot = "0.12"
|
||||
paste = { package = "pastey", version = "0.1" }
|
||||
substring = "1.4"
|
||||
terminal_size = "0.4"
|
||||
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||
tracing-actix-web = "0.7"
|
||||
|
||||
fluent-templates = "0.13"
|
||||
unic-langid = { version = "0.9", features = ["macros"] }
|
||||
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
actix-session = { version = "0.11", features = ["cookie-session"] }
|
||||
actix-web-files = { package = "actix-files", version = "0.6" }
|
||||
|
||||
serde.workspace = true
|
||||
|
||||
pagetop-macros.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
testing = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23"
|
||||
serde_json = "1.0"
|
||||
pagetop-aliner.workspace = true
|
||||
pagetop-bootsier.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
|
||||
|
||||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"pagetop",
|
||||
# Utilities.
|
||||
"pagetop-macros",
|
||||
"pagetop-build",
|
||||
"pagetop-jquery",
|
||||
"pagetop-homedemo",
|
||||
# Components.
|
||||
"pagetop-minimal",
|
||||
"pagetop-megamenu",
|
||||
# Modules.
|
||||
"pagetop-admin",
|
||||
"pagetop-user",
|
||||
"pagetop-node",
|
||||
# Themes.
|
||||
"pagetop-aliner",
|
||||
"pagetop-bootsier",
|
||||
"pagetop-bulmix",
|
||||
# Helpers
|
||||
"helpers/pagetop-build",
|
||||
"helpers/pagetop-macros",
|
||||
"helpers/pagetop-statics",
|
||||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"drust",
|
||||
"examples",
|
||||
"tests",
|
||||
]
|
||||
[workspace.package]
|
||||
repository = "https://git.cillero.es/manuelcillero/pagetop"
|
||||
homepage = "https://pagetop.cillero.es"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-web = { version = "4.11", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# Helpers
|
||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
||||
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.0", path = "extensions/pagetop-bootsier" }
|
||||
# PageTop
|
||||
pagetop = { version = "0.4", path = "." }
|
||||
|
|
|
|||
220
README.md
220
README.md
|
|
@ -1,105 +1,151 @@
|
|||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/banner-pagetop.png" />
|
||||
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
|
||||
|
||||
<h1>PageTop</h1>
|
||||
<h1>PageTop</h1>
|
||||
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://docs.rs/pagetop)
|
||||
<p>Un entorno para el desarrollo de soluciones web modulares, extensibles y configurables.</p>
|
||||
|
||||
[](https://docs.rs/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://crates.io/crates/pagetop)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
**PageTop** es un entorno de desarrollo basado en [Rust](https://www.rust-lang.org/es/) que reúne
|
||||
algunos de los crates más estables y populares para crear soluciones web modulares, extensibles y
|
||||
configurables.
|
||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
||||
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||
según las necesidades de cada proyecto, incluyendo:
|
||||
|
||||
Incluye **Drust**, un sistema de gestión de contenidos basado en PageTop que permite crear, editar y
|
||||
mantener sitios web dinámicos, rápidos y seguros.
|
||||
* **Acciones** (*actions*): alteran la lógica interna de una funcionalidad interceptando su flujo
|
||||
de ejecución.
|
||||
* **Componentes** (*components*): encapsulan HTML, CSS y JavaScript en unidades funcionales,
|
||||
configurables y reutilizables.
|
||||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
||||
de PageTop o de terceros.
|
||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||
componentes sin comprometer su funcionalidad.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
La aplicación más sencilla de PageTop se ve así:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::new().run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
||||
bienvenida accesible desde un navegador local en la dirección `http://localhost:8080`.
|
||||
|
||||
Para personalizar el servicio, se puede crear una extensión de PageTop de la siguiente manera:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Extension for HelloWorld {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
}
|
||||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&HelloWorld).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
||||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
||||
|
||||
|
||||
# 📂 Repositorio
|
||||
|
||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||
|
||||
* **[pagetop](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/src)**, con el código
|
||||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
||||
|
||||
## Auxiliares
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
|
||||
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
||||
|
||||
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
||||
proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
||||
|
||||
## Extensiones
|
||||
|
||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
||||
|
||||
* **[pagetop-bootsier](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier)**,
|
||||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
||||
componentes flexibles.
|
||||
|
||||
|
||||
# 🧪 Pruebas
|
||||
|
||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||
|
||||
| Comando | Descripción |
|
||||
| ------- | ----------- |
|
||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
||||
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
|
||||
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
|
||||
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
|
||||
|
||||
> **Nota**
|
||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender Rust y conocer su ecosistema. Sólo se liberan
|
||||
versiones de desarrollo. En este contexto la API no es estable y los cambios son constantes. No
|
||||
puede considerarse preparado hasta que se libere la versión **0.1.0**.
|
||||
|
||||
|
||||
# 📂 Estructura del código
|
||||
|
||||
El repositorio se organiza en un *workspace* con los siguientes subproyectos:
|
||||
|
||||
* **[pagetop](https://github.com/manuelcillero/pagetop/tree/main/pagetop)**, es la librería esencial
|
||||
construida con *crates* estables y muy conocidos del ecosistema Rust para proporcionar APIs,
|
||||
patrones de desarrollo y buenas prácticas para la creación avanzada de soluciones web SSR
|
||||
(*Server-Side Rendering*).
|
||||
|
||||
## Auxiliares
|
||||
|
||||
* **[pagetop-macros](https://github.com/manuelcillero/pagetop/tree/main/pagetop-macros)**, agrupa
|
||||
las principales macros procedurales para usar desde **PageTop**.
|
||||
|
||||
* **[pagetop-build](https://github.com/manuelcillero/pagetop/tree/main/pagetop-build)**, permite
|
||||
incluir fácilmente recursos en los archivos binarios al compilar aplicaciones creadas con
|
||||
**PageTop**.
|
||||
|
||||
## Componentes
|
||||
|
||||
* **[pagetop-minimal](https://github.com/manuelcillero/pagetop/tree/main/pagetop-minimal)**, módulo
|
||||
que proporciona un conjunto básico de componentes para la composición de páginas.
|
||||
|
||||
* **[pagetop-jquery](https://github.com/manuelcillero/pagetop/tree/main/pagetop-jquery)**, módulo
|
||||
que permite añadir jQuery en las páginas que incluyen componentes o temas que usen esta librería
|
||||
JavaScript para interactuar con el documento HTML.
|
||||
|
||||
* **[pagetop-megamenu](https://github.com/manuelcillero/pagetop/tree/main/pagetop-megamenu)**,
|
||||
módulo que proporciona un nuevo componente para incluir menús avanzados en las aplicaciones web
|
||||
creadas con **PageTop**.
|
||||
|
||||
## Módulos
|
||||
|
||||
* **[pagetop-homedemo](https://github.com/manuelcillero/pagetop/tree/main/pagetop-homedemo)**,
|
||||
módulo que muestra una página de inicio de demostración para presentar **PageTop**.
|
||||
|
||||
* **[pagetop-admin](https://github.com/manuelcillero/pagetop/tree/main/pagetop-admin)**, módulo que
|
||||
proporciona a otros módulos un lugar común donde presentar a los administradores sus opciones de
|
||||
configuración.
|
||||
|
||||
* **[pagetop-user](https://github.com/manuelcillero/pagetop/tree/main/pagetop-user)**, módulo para
|
||||
añadir gestión de usuarios, roles, permisos y sesiones en aplicaciones desarrolladas con PageTop.
|
||||
|
||||
* **[pagetop-node](https://github.com/manuelcillero/pagetop/tree/main/pagetop-node)**, módulo para
|
||||
crear, extender o personalizar los tipos de contenido que puede administrar un sitio web.
|
||||
|
||||
## Temas
|
||||
|
||||
* **[pagetop-aliner](https://github.com/manuelcillero/pagetop/tree/main/pagetop-aliner)**, tema que
|
||||
delimita con cajas los elementos HTML para mostrar esquemáticamente la composición de las páginas.
|
||||
|
||||
* **[pagetop-bootsier](https://github.com/manuelcillero/pagetop/tree/main/pagetop-bootsier)**, tema
|
||||
que utiliza el *framework* [Bootstrap](https://getbootstrap.com/) para la composición de páginas y
|
||||
visualización de componentes.
|
||||
|
||||
* **[pagetop-bulmix](https://github.com/manuelcillero/pagetop/tree/main/pagetop-bulmix)**, tema que
|
||||
utiliza el *framework* [Bulma](https://bulma.io/) para la composición de páginas y visualización
|
||||
de componentes.
|
||||
|
||||
## Aplicación
|
||||
|
||||
* **[drust](https://github.com/manuelcillero/pagetop/tree/main/drust)**, es una aplicación
|
||||
inspirada modestamente en [Drupal](https://www.drupal.org) que utiliza PageTop para crear un CMS
|
||||
(*Content Management System* o sistema de gestión de contenidos) para construir sitios web
|
||||
dinámicos, administrados y configurables.
|
||||
**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
|
||||
|
||||
Este proyecto tiene licencia, de hecho tiene dos, puedes aplicar cualquiera de las siguientes a tu
|
||||
elección:
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* Licencia Apache versión 2.0
|
||||
([LICENSE-APACHE](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-APACHE) o
|
||||
[http://www.apache.org/licenses/LICENSE-2.0]).
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* Licencia MIT
|
||||
([LICENSE-MIT](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-MIT) o
|
||||
[http://opensource.org/licenses/MIT]).
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
|
||||
|
||||
# ✨ Contribuir
|
||||
|
||||
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
|
||||
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la
|
||||
licencia *Apache v2.0*.
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static")
|
||||
.with_name("bootsier")
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("assets")
|
||||
.build()
|
||||
}
|
||||
2
config/default.toml
Normal file
2
config/default.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[log]
|
||||
tracing = "Info,pagetop=Debug"
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
[package]
|
||||
name = "drust"
|
||||
version = "0.0.3"
|
||||
edition = "2021"
|
||||
|
||||
authors = [
|
||||
"Manuel Cillero <manuel@cillero.es>"
|
||||
]
|
||||
description = """\
|
||||
A modern web Content Management System to share your world.\
|
||||
"""
|
||||
homepage = "https://pagetop.cillero.es"
|
||||
repository = "https://github.com/manuelcillero/pagetop"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
|
||||
[dependencies]
|
||||
pagetop = { version = "0.0", path = "../pagetop", features = ["mysql"], default-features = false }
|
||||
# Themes.
|
||||
pagetop-aliner = { version = "0.0", path = "../pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.0", path = "../pagetop-bootsier" }
|
||||
pagetop-bulmix = { version = "0.0", path = "../pagetop-bulmix" }
|
||||
# Modules.
|
||||
pagetop-homedemo = { version = "0.0", path = "../pagetop-homedemo" }
|
||||
pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
|
||||
pagetop-user = { version = "0.0", path = "../pagetop-user" }
|
||||
pagetop-node = { version = "0.0", path = "../pagetop-node" }
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
**Drust** es una aplicación inspirada modestamente en [Drupal](https://www.drupal.org) que utiliza
|
||||
**PageTop** para crear un CMS (*Content Management System* o sistema de gestión de contenidos) para
|
||||
construir sitios web dinámicos, administrados y configurables.
|
||||
|
||||
[PageTop](https://github.com/manuelcillero/pagetop/tree/main/pagetop), es un entorno de desarrollo
|
||||
basado en algunos de los *crates* más estables y populares del ecosistema Rust para proporcionar
|
||||
APIs, patrones de desarrollo y buenas prácticas para la creación de soluciones web SSR (*Server-Side
|
||||
Rendering*).
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
|
||||
**PageTop** sólo libera actualmente versiones de desarrollo. La API no es estable y los cambios son
|
||||
constantes. No puede considerarse preparado hasta que se libere la versión **0.1.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
|
||||
Este proyecto tiene licencia, de hecho tiene dos, puedes aplicar cualquiera de las siguientes a tu
|
||||
elección:
|
||||
|
||||
* Licencia Apache versión 2.0
|
||||
([LICENSE-APACHE](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-APACHE) o
|
||||
[http://www.apache.org/licenses/LICENSE-2.0]).
|
||||
|
||||
* Licencia MIT
|
||||
([LICENSE-MIT](https://github.com/manuelcillero/pagetop/blob/main/LICENSE-MIT) o
|
||||
[http://opensource.org/licenses/MIT]).
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[app]
|
||||
name = "Drust"
|
||||
description = """\
|
||||
A modern web Content Management System to share your world.\
|
||||
"""
|
||||
|
||||
[database]
|
||||
db_type = "mysql"
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[database]
|
||||
db_name = "drust"
|
||||
db_user = "drust"
|
||||
db_pass = "DrU__#3T"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[app]
|
||||
#theme = "Default"
|
||||
#theme = "Aliner"
|
||||
theme = "Bootsier"
|
||||
#theme = "Bulmix"
|
||||
language = "es-ES"
|
||||
|
||||
[log]
|
||||
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||
|
||||
[dev]
|
||||
#static_files = "../pagetop/static"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
new_handle!(APP_DRUST);
|
||||
|
||||
struct Drust;
|
||||
|
||||
impl ModuleTrait for Drust {
|
||||
fn handle(&self) -> Handle {
|
||||
APP_DRUST
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<ModuleRef> {
|
||||
vec![
|
||||
// Themes.
|
||||
&pagetop_aliner::Aliner,
|
||||
&pagetop_bootsier::Bootsier,
|
||||
&pagetop_bulmix::Bulmix,
|
||||
// Modules.
|
||||
&pagetop_homedemo::HomeDemo,
|
||||
&pagetop_admin::Admin,
|
||||
&pagetop_user::User,
|
||||
&pagetop_node::Node,
|
||||
]
|
||||
}
|
||||
|
||||
fn drop_modules(&self) -> Vec<ModuleRef> {
|
||||
vec![
|
||||
// &pagetop_node::Node
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&Drust).unwrap().run()?.await
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"basics/*",
|
||||
]
|
||||
6
examples/app-basic.rs
Normal file
6
examples/app-basic.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::new().run()?.await
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[package]
|
||||
name = "hello_name"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
pagetop = { version = "0.0", path = "../../../pagetop" }
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
new_handle!(APP_HELLO_NAME);
|
||||
|
||||
struct HelloName;
|
||||
|
||||
impl ModuleTrait for HelloName {
|
||||
fn handle(&self) -> Handle {
|
||||
APP_HELLO_NAME
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.service(hello_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[service::get("/hello/{name}")]
|
||||
async fn hello_name(
|
||||
request: service::HttpRequest,
|
||||
path: service::web::Path<String>,
|
||||
) -> ResultPage<Markup, FatalError> {
|
||||
let name = path.into_inner();
|
||||
Page::new(request)
|
||||
.with_in("content", Html::with(html! { h1 { "Hello " (name) "!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&HelloName).unwrap().run()?.await
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[package]
|
||||
name = "hello_world"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
pagetop = { version = "0.0", path = "../../../pagetop" }
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
new_handle!(APP_HELLO_WORLD);
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl ModuleTrait for HelloWorld {
|
||||
fn handle(&self) -> Handle {
|
||||
APP_HELLO_WORLD
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
}
|
||||
|
||||
async fn hello_world(request: service::HttpRequest) -> ResultPage<Markup, FatalError> {
|
||||
Page::new(request)
|
||||
.with_in("content", Html::with(html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&HelloWorld).unwrap().run()?.await
|
||||
}
|
||||
24
examples/hello-name.rs
Normal file
24
examples/hello-name.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
struct HelloName;
|
||||
|
||||
impl Extension for HelloName {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/hello/{name}", service::web::get().to(hello_name));
|
||||
}
|
||||
}
|
||||
|
||||
async fn hello_name(
|
||||
request: HttpRequest,
|
||||
path: service::web::Path<String>,
|
||||
) -> ResultPage<Markup, ErrorPage> {
|
||||
let name = path.into_inner();
|
||||
Page::new(request)
|
||||
.add_child(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&HelloName).run()?.await
|
||||
}
|
||||
20
examples/hello-world.rs
Normal file
20
examples/hello-world.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
struct HelloWorld;
|
||||
|
||||
impl Extension for HelloWorld {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(hello_world));
|
||||
}
|
||||
}
|
||||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&HelloWorld).run()?.await
|
||||
}
|
||||
109
examples/navbar-menus.rs
Normal file
109
examples/navbar-menus.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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
|
||||
}
|
||||
21
extensions/pagetop-aliner/Cargo.toml
Normal file
21
extensions/pagetop-aliner/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pagetop-aliner"
|
||||
version = "0.0.9"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML
|
||||
"""
|
||||
categories = ["web-programming", "gui"]
|
||||
keywords = ["pagetop", "theme", "css"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
201
extensions/pagetop-aliner/LICENSE-APACHE
Normal file
201
extensions/pagetop-aliner/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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.
|
||||
21
extensions/pagetop-aliner/LICENSE-MIT
Normal file
21
extensions/pagetop-aliner/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
101
extensions/pagetop-aliner/README.md
Normal file
101
extensions/pagetop-aliner/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Aliner</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-aliner = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_aliner::Aliner,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Aliner"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_aliner::Aliner;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Aliner)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_dir("./static")
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("aliner")
|
||||
.build()
|
||||
}
|
||||
115
extensions/pagetop-aliner/src/lib.rs
Normal file
115
extensions/pagetop-aliner/src/lib.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*!
|
||||
<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
extensions/pagetop-bootsier/.gitattributes
vendored
Normal file
1
extensions/pagetop-bootsier/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
static/** linguist-vendored
|
||||
22
extensions/pagetop-bootsier/Cargo.toml
Normal file
22
extensions/pagetop-bootsier/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "pagetop-bootsier"
|
||||
version = "0.0.18"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Tema de PageTop basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
||||
"""
|
||||
categories = ["web-programming", "gui"]
|
||||
keywords = ["pagetop", "theme", "bootstrap", "css", "js"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
pagetop-build.workspace = true
|
||||
201
extensions/pagetop-bootsier/LICENSE-APACHE
Normal file
201
extensions/pagetop-bootsier/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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.
|
||||
21
extensions/pagetop-bootsier/LICENSE-MIT
Normal file
21
extensions/pagetop-bootsier/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
101
extensions/pagetop-bootsier/README.md
Normal file
101
extensions/pagetop-bootsier/README.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Bootsier</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_bootsier::Bootsier,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::Bootsier;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Bootsier)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
20
extensions/pagetop-bootsier/build.rs
Normal file
20
extensions/pagetop-bootsier/build.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use pagetop_build::StaticFilesBundle;
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
StaticFilesBundle::from_scss("./static/scss/bootsier.scss", "bootstrap.min.css")
|
||||
.with_name("bootsier_bs")
|
||||
.build()?;
|
||||
StaticFilesBundle::from_dir("./static/js", Some(bootstrap_js_files))
|
||||
.with_name("bootsier_js")
|
||||
.build()
|
||||
}
|
||||
|
||||
fn bootstrap_js_files(path: &Path) -> bool {
|
||||
let bootstrap_js = "bootstrap.bundle.min.js";
|
||||
// No filtra durante el desarrollo, solo en la compilación "release".
|
||||
env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release"
|
||||
|| path.file_name().is_some_and(|f| f == bootstrap_js)
|
||||
}
|
||||
41
extensions/pagetop-bootsier/src/config.rs
Normal file
41
extensions/pagetop-bootsier/src/config.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! Opciones de configuración del tema.
|
||||
//!
|
||||
//! Ejemplo:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [bootsier]
|
||||
//! max_width = "90rem"
|
||||
//! ```
|
||||
//!
|
||||
//! Uso:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! use pagetop_bootsier::config;
|
||||
//!
|
||||
//! assert_eq!(config::SETTINGS.bootsier.max_width, UnitValue::Px(1440));
|
||||
//! ```
|
||||
//!
|
||||
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
|
||||
//! los valores a los ajustes.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
include_config!(SETTINGS: Settings => [
|
||||
// [bootsier]
|
||||
"bootsier.max_width" => "1440px",
|
||||
]);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Tipos para la sección [`[bootsier]`](Bootsier) de [`SETTINGS`].
|
||||
pub struct Settings {
|
||||
pub bootsier: Bootsier,
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Sección `[bootsier]` de la configuración. Forma parte de [`Settings`].
|
||||
pub struct Bootsier {
|
||||
/// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem".
|
||||
pub max_width: UnitValue,
|
||||
}
|
||||
132
extensions/pagetop-bootsier/src/lib.rs
Normal file
132
extensions/pagetop-bootsier/src/lib.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Bootsier</h1>
|
||||
|
||||
<p>Tema de <strong>PageTop</strong> basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
`dependencies()` determina la prioridad relativa frente a las otras extensiones:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
// ...
|
||||
&pagetop_bootsier::Bootsier,
|
||||
// ...
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
Y **selecciona el tema en la configuración** de la aplicación:
|
||||
|
||||
```toml
|
||||
[app]
|
||||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
use pagetop_bootsier::Bootsier;
|
||||
|
||||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Bootsier)
|
||||
.add_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOCALES_BOOTSIER);
|
||||
|
||||
// Versión de la librería Bootstrap.
|
||||
const BOOTSTRAP_VERSION: &str = "5.3.8";
|
||||
|
||||
pub mod config;
|
||||
|
||||
pub mod theme;
|
||||
|
||||
/// *Prelude* del tema.
|
||||
pub mod prelude {
|
||||
pub use crate::config::*;
|
||||
pub use crate::theme::*;
|
||||
}
|
||||
|
||||
/// Implementa el tema.
|
||||
pub struct Bootsier;
|
||||
|
||||
impl Extension for Bootsier {
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
static_files_service!(scfg, [bootsier_bs] => "/bootsier/bs");
|
||||
static_files_service!(scfg, [bootsier_js] => "/bootsier/js");
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme for Bootsier {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_assets(ContextOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,4 +3,3 @@ e404-message = The page you are looking for may have been removed, had its name
|
|||
e500-description = Oops! Unexpected Error
|
||||
e500-message = We're having an issue. Please report this error to an administrator.
|
||||
back-homepage = Back to homepage
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
|
||||
# Navbar
|
||||
toggle = Toggle navigation
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
|
||||
# Navbar
|
||||
toggle = Mostrar/ocultar navegación
|
||||
40
extensions/pagetop-bootsier/src/theme.rs
Normal file
40
extensions/pagetop-bootsier/src/theme.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
//! 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;
|
||||
20
extensions/pagetop-bootsier/src/theme/aux.rs
Normal file
20
extensions/pagetop-bootsier/src/theme/aux.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! 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};
|
||||
87
extensions/pagetop-bootsier/src/theme/aux/border.rs
Normal file
87
extensions/pagetop-bootsier/src/theme/aux/border.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
114
extensions/pagetop-bootsier/src/theme/aux/breakpoint.rs
Normal file
114
extensions/pagetop-bootsier/src/theme/aux/breakpoint.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
143
extensions/pagetop-bootsier/src/theme/aux/button.rs
Normal file
143
extensions/pagetop-bootsier/src/theme/aux/button.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
375
extensions/pagetop-bootsier/src/theme/aux/color.rs
Normal file
375
extensions/pagetop-bootsier/src/theme/aux/color.rs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
104
extensions/pagetop-bootsier/src/theme/aux/layout.rs
Normal file
104
extensions/pagetop-bootsier/src/theme/aux/layout.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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,
|
||||
}
|
||||
117
extensions/pagetop-bootsier/src/theme/aux/rounded.rs
Normal file
117
extensions/pagetop-bootsier/src/theme/aux/rounded.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
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("")
|
||||
}
|
||||
}
|
||||
13
extensions/pagetop-bootsier/src/theme/classes.rs
Normal file
13
extensions/pagetop-bootsier/src/theme/classes.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//! 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};
|
||||
175
extensions/pagetop-bootsier/src/theme/classes/border.rs
Normal file
175
extensions/pagetop-bootsier/src/theme/classes/border.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
230
extensions/pagetop-bootsier/src/theme/classes/color.rs
Normal file
230
extensions/pagetop-bootsier/src/theme/classes/color.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
205
extensions/pagetop-bootsier/src/theme/classes/layout.rs
Normal file
205
extensions/pagetop-bootsier/src/theme/classes/layout.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
169
extensions/pagetop-bootsier/src/theme/classes/rounded.rs
Normal file
169
extensions/pagetop-bootsier/src/theme/classes/rounded.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
24
extensions/pagetop-bootsier/src/theme/container.rs
Normal file
24
extensions/pagetop-bootsier/src/theme/container.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//! 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;
|
||||
184
extensions/pagetop-bootsier/src/theme/container/component.rs
Normal file
184
extensions/pagetop-bootsier/src/theme/container/component.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
72
extensions/pagetop-bootsier/src/theme/container/props.rs
Normal file
72
extensions/pagetop-bootsier/src/theme/container/props.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
extensions/pagetop-bootsier/src/theme/dropdown.rs
Normal file
34
extensions/pagetop-bootsier/src/theme/dropdown.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//! 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};
|
||||
302
extensions/pagetop-bootsier/src/theme/dropdown/component.rs
Normal file
302
extensions/pagetop-bootsier/src/theme/dropdown/component.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
281
extensions/pagetop-bootsier/src/theme/dropdown/item.rs
Normal file
281
extensions/pagetop-bootsier/src/theme/dropdown/item.rs
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
226
extensions/pagetop-bootsier/src/theme/dropdown/props.rs
Normal file
226
extensions/pagetop-bootsier/src/theme/dropdown/props.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
134
extensions/pagetop-bootsier/src/theme/icon.rs
Normal file
134
extensions/pagetop-bootsier/src/theme/icon.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
7
extensions/pagetop-bootsier/src/theme/image.rs
Normal file
7
extensions/pagetop-bootsier/src/theme/image.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! Definiciones para renderizar imágenes ([`Image`]).
|
||||
|
||||
mod props;
|
||||
pub use props::{Size, Source};
|
||||
|
||||
mod component;
|
||||
pub use component::Image;
|
||||
141
extensions/pagetop-bootsier/src/theme/image/component.rs
Normal file
141
extensions/pagetop-bootsier/src/theme/image/component.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
108
extensions/pagetop-bootsier/src/theme/image/props.rs
Normal file
108
extensions/pagetop-bootsier/src/theme/image/props.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
37
extensions/pagetop-bootsier/src/theme/nav.rs
Normal file
37
extensions/pagetop-bootsier/src/theme/nav.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//! 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};
|
||||
135
extensions/pagetop-bootsier/src/theme/nav/component.rs
Normal file
135
extensions/pagetop-bootsier/src/theme/nav/component.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
284
extensions/pagetop-bootsier/src/theme/nav/item.rs
Normal file
284
extensions/pagetop-bootsier/src/theme/nav/item.rs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
120
extensions/pagetop-bootsier/src/theme/nav/props.rs
Normal file
120
extensions/pagetop-bootsier/src/theme/nav/props.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
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()
|
||||
} */
|
||||
}
|
||||
136
extensions/pagetop-bootsier/src/theme/navbar.rs
Normal file
136
extensions/pagetop-bootsier/src/theme/navbar.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
//! 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;
|
||||
111
extensions/pagetop-bootsier/src/theme/navbar/brand.rs
Normal file
111
extensions/pagetop-bootsier/src/theme/navbar/brand.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
293
extensions/pagetop-bootsier/src/theme/navbar/component.rs
Normal file
293
extensions/pagetop-bootsier/src/theme/navbar/component.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
95
extensions/pagetop-bootsier/src/theme/navbar/item.rs
Normal file
95
extensions/pagetop-bootsier/src/theme/navbar/item.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
98
extensions/pagetop-bootsier/src/theme/navbar/props.rs
Normal file
98
extensions/pagetop-bootsier/src/theme/navbar/props.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
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()
|
||||
} */
|
||||
}
|
||||
27
extensions/pagetop-bootsier/src/theme/offcanvas.rs
Normal file
27
extensions/pagetop-bootsier/src/theme/offcanvas.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! 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;
|
||||
240
extensions/pagetop-bootsier/src/theme/offcanvas/component.rs
Normal file
240
extensions/pagetop-bootsier/src/theme/offcanvas/component.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
extensions/pagetop-bootsier/src/theme/offcanvas/props.rs
Normal file
119
extensions/pagetop-bootsier/src/theme/offcanvas/props.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
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()
|
||||
} */
|
||||
}
|
||||
7
extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4494
extensions/pagetop-bootsier/static/js/bootstrap.js
vendored
Normal file
4494
extensions/pagetop-bootsier/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
extensions/pagetop-bootsier/static/js/bootstrap.js.map
vendored
Normal file
1
extensions/pagetop-bootsier/static/js/bootstrap.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
108
extensions/pagetop-bootsier/static/scss/_customs.scss
vendored
Normal file
108
extensions/pagetop-bootsier/static/scss/_customs.scss
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Enable CSS Grid
|
||||
$enable-grid-classes: false;
|
||||
$enable-cssgrid: true;
|
||||
|
||||
// Opacity
|
||||
.bg-opacity-0 {
|
||||
--bs-bg-opacity: 0;
|
||||
}
|
||||
|
||||
.border-opacity-0 {
|
||||
--bs-border-opacity: 0;
|
||||
}
|
||||
|
||||
.text-opacity-0 {
|
||||
--bs-text-opacity: 0;
|
||||
}
|
||||
.text-opacity-10 {
|
||||
--bs-text-opacity: 0.1;
|
||||
}
|
||||
|
||||
// Extending utilities
|
||||
$utilities: map-merge(
|
||||
$utilities,
|
||||
(
|
||||
// Individual border widths
|
||||
"border-top": (
|
||||
property: border-top-width,
|
||||
class: border-top,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-end": (
|
||||
property: border-right-width,
|
||||
class: border-end,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-bottom": (
|
||||
property: border-bottom-width,
|
||||
class: border-bottom,
|
||||
values: $border-widths
|
||||
),
|
||||
"border-start": (
|
||||
property: border-left-width,
|
||||
class: border-start,
|
||||
values: $border-widths
|
||||
),
|
||||
// Individual rounded values
|
||||
"rounded-top-start": (
|
||||
property: border-top-left-radius,
|
||||
class: rounded-top-start,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-top-end": (
|
||||
property: border-top-right-radius,
|
||||
class: rounded-top-end,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-bottom-start": (
|
||||
property: border-bottom-left-radius,
|
||||
class: rounded-bottom-start,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
"rounded-bottom-end": (
|
||||
property: border-bottom-right-radius,
|
||||
class: rounded-bottom-end,
|
||||
values: (
|
||||
null: var(--#{$prefix}border-radius),
|
||||
0: 0,
|
||||
1: var(--#{$prefix}border-radius-sm),
|
||||
2: var(--#{$prefix}border-radius),
|
||||
3: var(--#{$prefix}border-radius-lg),
|
||||
4: var(--#{$prefix}border-radius-xl),
|
||||
5: var(--#{$prefix}border-radius-xxl),
|
||||
circle: 50%,
|
||||
pill: var(--#{$prefix}border-radius-pill)
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
55
extensions/pagetop-bootsier/static/scss/bootsier.scss
vendored
Normal file
55
extensions/pagetop-bootsier/static/scss/bootsier.scss
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@import "bootstrap-5.3.8/mixins/banner";
|
||||
@include bsBanner("");
|
||||
|
||||
|
||||
// scss-docs-start import-stack
|
||||
// Configuration
|
||||
@import "bootstrap-5.3.8/functions";
|
||||
@import "bootstrap-5.3.8/variables";
|
||||
@import "bootstrap-5.3.8/variables-dark";
|
||||
@import "bootstrap-5.3.8/maps";
|
||||
@import "bootstrap-5.3.8/mixins";
|
||||
@import "bootstrap-5.3.8/utilities";
|
||||
|
||||
// Layout & components
|
||||
@import "bootstrap-5.3.8/root";
|
||||
@import "bootstrap-5.3.8/reboot";
|
||||
@import "bootstrap-5.3.8/type";
|
||||
@import "bootstrap-5.3.8/images";
|
||||
@import "bootstrap-5.3.8/containers";
|
||||
@import "bootstrap-5.3.8/grid";
|
||||
@import "bootstrap-5.3.8/tables";
|
||||
@import "bootstrap-5.3.8/forms";
|
||||
@import "bootstrap-5.3.8/buttons";
|
||||
@import "bootstrap-5.3.8/transitions";
|
||||
@import "bootstrap-5.3.8/dropdown";
|
||||
@import "bootstrap-5.3.8/button-group";
|
||||
@import "bootstrap-5.3.8/nav";
|
||||
@import "bootstrap-5.3.8/navbar";
|
||||
@import "bootstrap-5.3.8/card";
|
||||
@import "bootstrap-5.3.8/accordion";
|
||||
@import "bootstrap-5.3.8/breadcrumb";
|
||||
@import "bootstrap-5.3.8/pagination";
|
||||
@import "bootstrap-5.3.8/badge";
|
||||
@import "bootstrap-5.3.8/alert";
|
||||
@import "bootstrap-5.3.8/progress";
|
||||
@import "bootstrap-5.3.8/list-group";
|
||||
@import "bootstrap-5.3.8/close";
|
||||
@import "bootstrap-5.3.8/toasts";
|
||||
@import "bootstrap-5.3.8/modal";
|
||||
@import "bootstrap-5.3.8/tooltip";
|
||||
@import "bootstrap-5.3.8/popover";
|
||||
@import "bootstrap-5.3.8/carousel";
|
||||
@import "bootstrap-5.3.8/spinners";
|
||||
@import "bootstrap-5.3.8/offcanvas";
|
||||
@import "bootstrap-5.3.8/placeholders";
|
||||
|
||||
// Helpers
|
||||
@import "bootstrap-5.3.8/helpers";
|
||||
|
||||
// Custom definitions
|
||||
@import "customs";
|
||||
|
||||
// Utilities
|
||||
@import "bootstrap-5.3.8/utilities/api";
|
||||
// scss-docs-end import-stack
|
||||
153
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_accordion.scss
vendored
Normal file
153
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_accordion.scss
vendored
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.accordion {
|
||||
// scss-docs-start accordion-css-vars
|
||||
--#{$prefix}accordion-color: #{$accordion-color};
|
||||
--#{$prefix}accordion-bg: #{$accordion-bg};
|
||||
--#{$prefix}accordion-transition: #{$accordion-transition};
|
||||
--#{$prefix}accordion-border-color: #{$accordion-border-color};
|
||||
--#{$prefix}accordion-border-width: #{$accordion-border-width};
|
||||
--#{$prefix}accordion-border-radius: #{$accordion-border-radius};
|
||||
--#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius};
|
||||
--#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x};
|
||||
--#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y};
|
||||
--#{$prefix}accordion-btn-color: #{$accordion-button-color};
|
||||
--#{$prefix}accordion-btn-bg: #{$accordion-button-bg};
|
||||
--#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)};
|
||||
--#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width};
|
||||
--#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform};
|
||||
--#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition};
|
||||
--#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)};
|
||||
--#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow};
|
||||
--#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x};
|
||||
--#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y};
|
||||
--#{$prefix}accordion-active-color: #{$accordion-button-active-color};
|
||||
--#{$prefix}accordion-active-bg: #{$accordion-button-active-bg};
|
||||
// scss-docs-end accordion-css-vars
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x);
|
||||
@include font-size($font-size-base);
|
||||
color: var(--#{$prefix}accordion-btn-color);
|
||||
text-align: left; // Reset button style
|
||||
background-color: var(--#{$prefix}accordion-btn-bg);
|
||||
border: 0;
|
||||
@include border-radius(0);
|
||||
overflow-anchor: none;
|
||||
@include transition(var(--#{$prefix}accordion-transition));
|
||||
|
||||
&:not(.collapsed) {
|
||||
color: var(--#{$prefix}accordion-active-color);
|
||||
background-color: var(--#{$prefix}accordion-active-bg);
|
||||
box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list
|
||||
|
||||
&::after {
|
||||
background-image: var(--#{$prefix}accordion-btn-active-icon);
|
||||
transform: var(--#{$prefix}accordion-btn-icon-transform);
|
||||
}
|
||||
}
|
||||
|
||||
// Accordion icon
|
||||
&::after {
|
||||
flex-shrink: 0;
|
||||
width: var(--#{$prefix}accordion-btn-icon-width);
|
||||
height: var(--#{$prefix}accordion-btn-icon-width);
|
||||
margin-left: auto;
|
||||
content: "";
|
||||
background-image: var(--#{$prefix}accordion-btn-icon);
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--#{$prefix}accordion-btn-icon-width);
|
||||
@include transition(var(--#{$prefix}accordion-btn-icon-transition));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
z-index: 3;
|
||||
outline: 0;
|
||||
box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
color: var(--#{$prefix}accordion-color);
|
||||
background-color: var(--#{$prefix}accordion-bg);
|
||||
border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color);
|
||||
|
||||
&:first-of-type {
|
||||
@include border-top-radius(var(--#{$prefix}accordion-border-radius));
|
||||
|
||||
> .accordion-header .accordion-button {
|
||||
@include border-top-radius(var(--#{$prefix}accordion-inner-border-radius));
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
// Only set a border-radius on the last item if the accordion is collapsed
|
||||
&:last-of-type {
|
||||
@include border-bottom-radius(var(--#{$prefix}accordion-border-radius));
|
||||
|
||||
> .accordion-header .accordion-button {
|
||||
&.collapsed {
|
||||
@include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius));
|
||||
}
|
||||
}
|
||||
|
||||
> .accordion-collapse {
|
||||
@include border-bottom-radius(var(--#{$prefix}accordion-border-radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x);
|
||||
}
|
||||
|
||||
|
||||
// Flush accordion items
|
||||
//
|
||||
// Remove borders and border-radius to keep accordion items edge-to-edge.
|
||||
|
||||
.accordion-flush {
|
||||
> .accordion-item {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
@include border-radius(0);
|
||||
|
||||
&:first-child { border-top: 0; }
|
||||
&:last-child { border-bottom: 0; }
|
||||
|
||||
// stylelint-disable selector-max-class
|
||||
> .accordion-collapse,
|
||||
> .accordion-header .accordion-button,
|
||||
> .accordion-header .accordion-button.collapsed {
|
||||
@include border-radius(0);
|
||||
}
|
||||
// stylelint-enable selector-max-class
|
||||
}
|
||||
}
|
||||
|
||||
@if $enable-dark-mode {
|
||||
@include color-mode(dark) {
|
||||
.accordion-button::after {
|
||||
--#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)};
|
||||
--#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)};
|
||||
}
|
||||
}
|
||||
}
|
||||
68
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_alert.scss
vendored
Normal file
68
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_alert.scss
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.alert {
|
||||
// scss-docs-start alert-css-vars
|
||||
--#{$prefix}alert-bg: transparent;
|
||||
--#{$prefix}alert-padding-x: #{$alert-padding-x};
|
||||
--#{$prefix}alert-padding-y: #{$alert-padding-y};
|
||||
--#{$prefix}alert-margin-bottom: #{$alert-margin-bottom};
|
||||
--#{$prefix}alert-color: inherit;
|
||||
--#{$prefix}alert-border-color: transparent;
|
||||
--#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color);
|
||||
--#{$prefix}alert-border-radius: #{$alert-border-radius};
|
||||
--#{$prefix}alert-link-color: inherit;
|
||||
// scss-docs-end alert-css-vars
|
||||
|
||||
position: relative;
|
||||
padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x);
|
||||
margin-bottom: var(--#{$prefix}alert-margin-bottom);
|
||||
color: var(--#{$prefix}alert-color);
|
||||
background-color: var(--#{$prefix}alert-bg);
|
||||
border: var(--#{$prefix}alert-border);
|
||||
@include border-radius(var(--#{$prefix}alert-border-radius));
|
||||
}
|
||||
|
||||
// Headings for larger alerts
|
||||
.alert-heading {
|
||||
// Specified to prevent conflicts of changing $headings-color
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// Provide class for links that match alerts
|
||||
.alert-link {
|
||||
font-weight: $alert-link-font-weight;
|
||||
color: var(--#{$prefix}alert-link-color);
|
||||
}
|
||||
|
||||
|
||||
// Dismissible alerts
|
||||
//
|
||||
// Expand the right padding and account for the close button's positioning.
|
||||
|
||||
.alert-dismissible {
|
||||
padding-right: $alert-dismissible-padding-r;
|
||||
|
||||
// Adjust close link position
|
||||
.btn-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: $stretched-link-z-index + 1;
|
||||
padding: $alert-padding-y * 1.25 $alert-padding-x;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// scss-docs-start alert-modifiers
|
||||
// Generate contextual modifier classes for colorizing the alert
|
||||
@each $state in map-keys($theme-colors) {
|
||||
.alert-#{$state} {
|
||||
--#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis);
|
||||
--#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle);
|
||||
--#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle);
|
||||
--#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis);
|
||||
}
|
||||
}
|
||||
// scss-docs-end alert-modifiers
|
||||
38
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_badge.scss
vendored
Normal file
38
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_badge.scss
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Base class
|
||||
//
|
||||
// Requires one of the contextual, color modifier classes for `color` and
|
||||
// `background-color`.
|
||||
|
||||
.badge {
|
||||
// scss-docs-start badge-css-vars
|
||||
--#{$prefix}badge-padding-x: #{$badge-padding-x};
|
||||
--#{$prefix}badge-padding-y: #{$badge-padding-y};
|
||||
@include rfs($badge-font-size, --#{$prefix}badge-font-size);
|
||||
--#{$prefix}badge-font-weight: #{$badge-font-weight};
|
||||
--#{$prefix}badge-color: #{$badge-color};
|
||||
--#{$prefix}badge-border-radius: #{$badge-border-radius};
|
||||
// scss-docs-end badge-css-vars
|
||||
|
||||
display: inline-block;
|
||||
padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x);
|
||||
@include font-size(var(--#{$prefix}badge-font-size));
|
||||
font-weight: var(--#{$prefix}badge-font-weight);
|
||||
line-height: 1;
|
||||
color: var(--#{$prefix}badge-color);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
@include border-radius(var(--#{$prefix}badge-border-radius));
|
||||
@include gradient-bg();
|
||||
|
||||
// Empty badges collapse automatically
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Quick fix for badges in buttons
|
||||
.btn .badge {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
40
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_breadcrumb.scss
vendored
Normal file
40
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_breadcrumb.scss
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.breadcrumb {
|
||||
// scss-docs-start breadcrumb-css-vars
|
||||
--#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x};
|
||||
--#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y};
|
||||
--#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom};
|
||||
@include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size);
|
||||
--#{$prefix}breadcrumb-bg: #{$breadcrumb-bg};
|
||||
--#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius};
|
||||
--#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color};
|
||||
--#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x};
|
||||
--#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color};
|
||||
// scss-docs-end breadcrumb-css-vars
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x);
|
||||
margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom);
|
||||
@include font-size(var(--#{$prefix}breadcrumb-font-size));
|
||||
list-style: none;
|
||||
background-color: var(--#{$prefix}breadcrumb-bg);
|
||||
@include border-radius(var(--#{$prefix}breadcrumb-border-radius));
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
// The separator between breadcrumbs (by default, a forward-slash: "/")
|
||||
+ .breadcrumb-item {
|
||||
padding-left: var(--#{$prefix}breadcrumb-item-padding-x);
|
||||
|
||||
&::before {
|
||||
float: left; // Suppress inline spacings and underlining of the separator
|
||||
padding-right: var(--#{$prefix}breadcrumb-item-padding-x);
|
||||
color: var(--#{$prefix}breadcrumb-divider-color);
|
||||
content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"};
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--#{$prefix}breadcrumb-item-active-color);
|
||||
}
|
||||
}
|
||||
147
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_button-group.scss
vendored
Normal file
147
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_button-group.scss
vendored
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Make the div behave like a button
|
||||
.btn-group,
|
||||
.btn-group-vertical {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle; // match .btn alignment given font-size hack above
|
||||
|
||||
> .btn {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
// Bring the hover, focused, and "active" buttons to the front to overlay
|
||||
// the borders properly
|
||||
> .btn-check:checked + .btn,
|
||||
> .btn-check:focus + .btn,
|
||||
> .btn:hover,
|
||||
> .btn:focus,
|
||||
> .btn:active,
|
||||
> .btn.active {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Group multiple button groups together for a toolbar
|
||||
.btn-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
.input-group {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
@include border-radius($btn-border-radius);
|
||||
|
||||
// Prevent double borders when buttons are next to each other
|
||||
> :not(.btn-check:first-child) + .btn,
|
||||
> .btn-group:not(:first-child) {
|
||||
margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list
|
||||
}
|
||||
|
||||
// Reset rounded corners
|
||||
> .btn:not(:last-child):not(.dropdown-toggle),
|
||||
> .btn.dropdown-toggle-split:first-child,
|
||||
> .btn-group:not(:last-child) > .btn {
|
||||
@include border-end-radius(0);
|
||||
}
|
||||
|
||||
// The left radius should be 0 if the button is:
|
||||
// - the "third or more" child
|
||||
// - the second child and the previous element isn't `.btn-check` (making it the first child visually)
|
||||
// - part of a btn-group which isn't the first child
|
||||
> .btn:nth-child(n + 3),
|
||||
> :not(.btn-check) + .btn,
|
||||
> .btn-group:not(:first-child) > .btn {
|
||||
@include border-start-radius(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Sizing
|
||||
//
|
||||
// Remix the default button sizing classes into new ones for easier manipulation.
|
||||
|
||||
.btn-group-sm > .btn { @extend .btn-sm; }
|
||||
.btn-group-lg > .btn { @extend .btn-lg; }
|
||||
|
||||
|
||||
//
|
||||
// Split button dropdowns
|
||||
//
|
||||
|
||||
.dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x * .75;
|
||||
padding-left: $btn-padding-x * .75;
|
||||
|
||||
&::after,
|
||||
.dropup &::after,
|
||||
.dropend &::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dropstart &::before {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sm + .dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x-sm * .75;
|
||||
padding-left: $btn-padding-x-sm * .75;
|
||||
}
|
||||
|
||||
.btn-lg + .dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x-lg * .75;
|
||||
padding-left: $btn-padding-x-lg * .75;
|
||||
}
|
||||
|
||||
|
||||
// The clickable button for toggling the menu
|
||||
// Set the same inset shadow as the :active state
|
||||
.btn-group.show .dropdown-toggle {
|
||||
@include box-shadow($btn-active-box-shadow);
|
||||
|
||||
// Show no shadow for `.btn-link` since it has no other button styles.
|
||||
&.btn-link {
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Vertical button groups
|
||||
//
|
||||
|
||||
.btn-group-vertical {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
> .btn,
|
||||
> .btn-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .btn:not(:first-child),
|
||||
> .btn-group:not(:first-child) {
|
||||
margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list
|
||||
}
|
||||
|
||||
// Reset rounded corners
|
||||
> .btn:not(:last-child):not(.dropdown-toggle),
|
||||
> .btn-group:not(:last-child) > .btn {
|
||||
@include border-bottom-radius(0);
|
||||
}
|
||||
|
||||
// The top radius should be 0 if the button is:
|
||||
// - the "third or more" child
|
||||
// - the second child and the previous element isn't `.btn-check` (making it the first child visually)
|
||||
// - part of a btn-group which isn't the first child
|
||||
> .btn:nth-child(n + 3),
|
||||
> :not(.btn-check) + .btn,
|
||||
> .btn-group:not(:first-child) > .btn {
|
||||
@include border-top-radius(0);
|
||||
}
|
||||
}
|
||||
216
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_buttons.scss
vendored
Normal file
216
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_buttons.scss
vendored
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.btn {
|
||||
// scss-docs-start btn-css-vars
|
||||
--#{$prefix}btn-padding-x: #{$btn-padding-x};
|
||||
--#{$prefix}btn-padding-y: #{$btn-padding-y};
|
||||
--#{$prefix}btn-font-family: #{$btn-font-family};
|
||||
@include rfs($btn-font-size, --#{$prefix}btn-font-size);
|
||||
--#{$prefix}btn-font-weight: #{$btn-font-weight};
|
||||
--#{$prefix}btn-line-height: #{$btn-line-height};
|
||||
--#{$prefix}btn-color: #{$btn-color};
|
||||
--#{$prefix}btn-bg: transparent;
|
||||
--#{$prefix}btn-border-width: #{$btn-border-width};
|
||||
--#{$prefix}btn-border-color: transparent;
|
||||
--#{$prefix}btn-border-radius: #{$btn-border-radius};
|
||||
--#{$prefix}btn-hover-border-color: transparent;
|
||||
--#{$prefix}btn-box-shadow: #{$btn-box-shadow};
|
||||
--#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity};
|
||||
--#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5);
|
||||
// scss-docs-end btn-css-vars
|
||||
|
||||
display: inline-block;
|
||||
padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x);
|
||||
font-family: var(--#{$prefix}btn-font-family);
|
||||
@include font-size(var(--#{$prefix}btn-font-size));
|
||||
font-weight: var(--#{$prefix}btn-font-weight);
|
||||
line-height: var(--#{$prefix}btn-line-height);
|
||||
color: var(--#{$prefix}btn-color);
|
||||
text-align: center;
|
||||
text-decoration: if($link-decoration == none, null, none);
|
||||
white-space: $btn-white-space;
|
||||
vertical-align: middle;
|
||||
cursor: if($enable-button-pointers, pointer, null);
|
||||
user-select: none;
|
||||
border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color);
|
||||
@include border-radius(var(--#{$prefix}btn-border-radius));
|
||||
@include gradient-bg(var(--#{$prefix}btn-bg));
|
||||
@include box-shadow(var(--#{$prefix}btn-box-shadow));
|
||||
@include transition($btn-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--#{$prefix}btn-hover-color);
|
||||
text-decoration: if($link-hover-decoration == underline, none, null);
|
||||
background-color: var(--#{$prefix}btn-hover-bg);
|
||||
border-color: var(--#{$prefix}btn-hover-border-color);
|
||||
}
|
||||
|
||||
.btn-check + &:hover {
|
||||
// override for the checkbox/radio buttons
|
||||
color: var(--#{$prefix}btn-color);
|
||||
background-color: var(--#{$prefix}btn-bg);
|
||||
border-color: var(--#{$prefix}btn-border-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--#{$prefix}btn-hover-color);
|
||||
@include gradient-bg(var(--#{$prefix}btn-hover-bg));
|
||||
border-color: var(--#{$prefix}btn-hover-border-color);
|
||||
outline: 0;
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
|
||||
} @else {
|
||||
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-check:focus-visible + & {
|
||||
border-color: var(--#{$prefix}btn-hover-border-color);
|
||||
outline: 0;
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
|
||||
} @else {
|
||||
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-check:checked + &,
|
||||
:not(.btn-check) + &:active,
|
||||
&:first-child:active,
|
||||
&.active,
|
||||
&.show {
|
||||
color: var(--#{$prefix}btn-active-color);
|
||||
background-color: var(--#{$prefix}btn-active-bg);
|
||||
// Remove CSS gradients if they're enabled
|
||||
background-image: if($enable-gradients, none, null);
|
||||
border-color: var(--#{$prefix}btn-active-border-color);
|
||||
@include box-shadow(var(--#{$prefix}btn-active-shadow));
|
||||
|
||||
&:focus-visible {
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow);
|
||||
} @else {
|
||||
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-check:checked:focus-visible + & {
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow);
|
||||
} @else {
|
||||
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled,
|
||||
fieldset:disabled & {
|
||||
color: var(--#{$prefix}btn-disabled-color);
|
||||
pointer-events: none;
|
||||
background-color: var(--#{$prefix}btn-disabled-bg);
|
||||
background-image: if($enable-gradients, none, null);
|
||||
border-color: var(--#{$prefix}btn-disabled-border-color);
|
||||
opacity: var(--#{$prefix}btn-disabled-opacity);
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Alternate buttons
|
||||
//
|
||||
|
||||
// scss-docs-start btn-variant-loops
|
||||
@each $color, $value in $theme-colors {
|
||||
.btn-#{$color} {
|
||||
@if $color == "light" {
|
||||
@include button-variant(
|
||||
$value,
|
||||
$value,
|
||||
$hover-background: shade-color($value, $btn-hover-bg-shade-amount),
|
||||
$hover-border: shade-color($value, $btn-hover-border-shade-amount),
|
||||
$active-background: shade-color($value, $btn-active-bg-shade-amount),
|
||||
$active-border: shade-color($value, $btn-active-border-shade-amount)
|
||||
);
|
||||
} @else if $color == "dark" {
|
||||
@include button-variant(
|
||||
$value,
|
||||
$value,
|
||||
$hover-background: tint-color($value, $btn-hover-bg-tint-amount),
|
||||
$hover-border: tint-color($value, $btn-hover-border-tint-amount),
|
||||
$active-background: tint-color($value, $btn-active-bg-tint-amount),
|
||||
$active-border: tint-color($value, $btn-active-border-tint-amount)
|
||||
);
|
||||
} @else {
|
||||
@include button-variant($value, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.btn-outline-#{$color} {
|
||||
@include button-outline-variant($value);
|
||||
}
|
||||
}
|
||||
// scss-docs-end btn-variant-loops
|
||||
|
||||
|
||||
//
|
||||
// Link buttons
|
||||
//
|
||||
|
||||
// Make a button look and behave like a link
|
||||
.btn-link {
|
||||
--#{$prefix}btn-font-weight: #{$font-weight-normal};
|
||||
--#{$prefix}btn-color: #{$btn-link-color};
|
||||
--#{$prefix}btn-bg: transparent;
|
||||
--#{$prefix}btn-border-color: transparent;
|
||||
--#{$prefix}btn-hover-color: #{$btn-link-hover-color};
|
||||
--#{$prefix}btn-hover-border-color: transparent;
|
||||
--#{$prefix}btn-active-color: #{$btn-link-hover-color};
|
||||
--#{$prefix}btn-active-border-color: transparent;
|
||||
--#{$prefix}btn-disabled-color: #{$btn-link-disabled-color};
|
||||
--#{$prefix}btn-disabled-border-color: transparent;
|
||||
--#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows
|
||||
--#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb};
|
||||
|
||||
text-decoration: $link-decoration;
|
||||
@if $enable-gradients {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
text-decoration: $link-hover-decoration;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--#{$prefix}btn-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--#{$prefix}btn-hover-color);
|
||||
}
|
||||
|
||||
// No need for an active state here
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Button Sizes
|
||||
//
|
||||
|
||||
.btn-lg {
|
||||
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
|
||||
}
|
||||
238
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_card.scss
vendored
Normal file
238
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_card.scss
vendored
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.card {
|
||||
// scss-docs-start card-css-vars
|
||||
--#{$prefix}card-spacer-y: #{$card-spacer-y};
|
||||
--#{$prefix}card-spacer-x: #{$card-spacer-x};
|
||||
--#{$prefix}card-title-spacer-y: #{$card-title-spacer-y};
|
||||
--#{$prefix}card-title-color: #{$card-title-color};
|
||||
--#{$prefix}card-subtitle-color: #{$card-subtitle-color};
|
||||
--#{$prefix}card-border-width: #{$card-border-width};
|
||||
--#{$prefix}card-border-color: #{$card-border-color};
|
||||
--#{$prefix}card-border-radius: #{$card-border-radius};
|
||||
--#{$prefix}card-box-shadow: #{$card-box-shadow};
|
||||
--#{$prefix}card-inner-border-radius: #{$card-inner-border-radius};
|
||||
--#{$prefix}card-cap-padding-y: #{$card-cap-padding-y};
|
||||
--#{$prefix}card-cap-padding-x: #{$card-cap-padding-x};
|
||||
--#{$prefix}card-cap-bg: #{$card-cap-bg};
|
||||
--#{$prefix}card-cap-color: #{$card-cap-color};
|
||||
--#{$prefix}card-height: #{$card-height};
|
||||
--#{$prefix}card-color: #{$card-color};
|
||||
--#{$prefix}card-bg: #{$card-bg};
|
||||
--#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding};
|
||||
--#{$prefix}card-group-margin: #{$card-group-margin};
|
||||
// scss-docs-end card-css-vars
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106
|
||||
height: var(--#{$prefix}card-height);
|
||||
color: var(--#{$prefix}body-color);
|
||||
word-wrap: break-word;
|
||||
background-color: var(--#{$prefix}card-bg);
|
||||
background-clip: border-box;
|
||||
border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
|
||||
@include border-radius(var(--#{$prefix}card-border-radius));
|
||||
@include box-shadow(var(--#{$prefix}card-box-shadow));
|
||||
|
||||
> hr {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
> .list-group {
|
||||
border-top: inherit;
|
||||
border-bottom: inherit;
|
||||
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
@include border-top-radius(var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
@include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
}
|
||||
|
||||
// Due to specificity of the above selector (`.card > .list-group`), we must
|
||||
// use a child selector here to prevent double borders.
|
||||
> .card-header + .list-group,
|
||||
> .list-group + .card-footer {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
// Enable `flex-grow: 1` for decks and groups so that card blocks take up
|
||||
// as much space as possible, ensuring footers are aligned to the bottom.
|
||||
flex: 1 1 auto;
|
||||
padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x);
|
||||
color: var(--#{$prefix}card-color);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: var(--#{$prefix}card-title-spacer-y);
|
||||
color: var(--#{$prefix}card-title-color);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list
|
||||
margin-bottom: 0;
|
||||
color: var(--#{$prefix}card-subtitle-color);
|
||||
}
|
||||
|
||||
.card-text:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
&:hover {
|
||||
text-decoration: if($link-hover-decoration == underline, none, null);
|
||||
}
|
||||
|
||||
+ .card-link {
|
||||
margin-left: var(--#{$prefix}card-spacer-x);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Optional textual caps
|
||||
//
|
||||
|
||||
.card-header {
|
||||
padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
|
||||
margin-bottom: 0; // Removes the default margin-bottom of <hN>
|
||||
color: var(--#{$prefix}card-cap-color);
|
||||
background-color: var(--#{$prefix}card-cap-bg);
|
||||
border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
|
||||
|
||||
&:first-child {
|
||||
@include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
|
||||
color: var(--#{$prefix}card-cap-color);
|
||||
background-color: var(--#{$prefix}card-cap-bg);
|
||||
border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
|
||||
|
||||
&:last-child {
|
||||
@include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Header navs
|
||||
//
|
||||
|
||||
.card-header-tabs {
|
||||
margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
|
||||
margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list
|
||||
margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
|
||||
border-bottom: 0;
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--#{$prefix}card-bg);
|
||||
border-bottom-color: var(--#{$prefix}card-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-pills {
|
||||
margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
|
||||
margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
|
||||
}
|
||||
|
||||
// Card image
|
||||
.card-img-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: var(--#{$prefix}card-img-overlay-padding);
|
||||
@include border-radius(var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
|
||||
.card-img,
|
||||
.card-img-top,
|
||||
.card-img-bottom {
|
||||
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
|
||||
}
|
||||
|
||||
.card-img,
|
||||
.card-img-top {
|
||||
@include border-top-radius(var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
|
||||
.card-img,
|
||||
.card-img-bottom {
|
||||
@include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Card groups
|
||||
//
|
||||
|
||||
.card-group {
|
||||
// The child selector allows nested `.card` within `.card-group`
|
||||
// to display properly.
|
||||
> .card {
|
||||
margin-bottom: var(--#{$prefix}card-group-margin);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
// The child selector allows nested `.card` within `.card-group`
|
||||
// to display properly.
|
||||
> .card {
|
||||
flex: 1 0 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
+ .card {
|
||||
margin-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
// Handle rounded corners
|
||||
@if $enable-rounded {
|
||||
&:not(:last-child) {
|
||||
@include border-end-radius(0);
|
||||
|
||||
> .card-img-top,
|
||||
> .card-header {
|
||||
// stylelint-disable-next-line property-disallowed-list
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
> .card-img-bottom,
|
||||
> .card-footer {
|
||||
// stylelint-disable-next-line property-disallowed-list
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
@include border-start-radius(0);
|
||||
|
||||
> .card-img-top,
|
||||
> .card-header {
|
||||
// stylelint-disable-next-line property-disallowed-list
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
> .card-img-bottom,
|
||||
> .card-footer {
|
||||
// stylelint-disable-next-line property-disallowed-list
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
226
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_carousel.scss
vendored
Normal file
226
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_carousel.scss
vendored
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
// Notes on the classes:
|
||||
//
|
||||
// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)
|
||||
// even when their scroll action started on a carousel, but for compatibility (with Firefox)
|
||||
// we're preventing all actions instead
|
||||
// 2. The .carousel-item-start and .carousel-item-end is used to indicate where
|
||||
// the active slide is heading.
|
||||
// 3. .active.carousel-item is the current slide.
|
||||
// 4. .active.carousel-item-start and .active.carousel-item-end is the current
|
||||
// slide in its in-transition state. Only one of these occurs at a time.
|
||||
// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end
|
||||
// is the upcoming slide in transition.
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel.pointer-event {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
position: relative;
|
||||
display: none;
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
backface-visibility: hidden;
|
||||
@include transition($carousel-transition);
|
||||
}
|
||||
|
||||
.carousel-item.active,
|
||||
.carousel-item-next,
|
||||
.carousel-item-prev {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.carousel-item-next:not(.carousel-item-start),
|
||||
.active.carousel-item-end {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.carousel-item-prev:not(.carousel-item-end),
|
||||
.active.carousel-item-start {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Alternate transitions
|
||||
//
|
||||
|
||||
.carousel-fade {
|
||||
.carousel-item {
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.carousel-item.active,
|
||||
.carousel-item-next.carousel-item-start,
|
||||
.carousel-item-prev.carousel-item-end {
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active.carousel-item-start,
|
||||
.active.carousel-item-end {
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
@include transition(opacity 0s $carousel-transition-duration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Left/right controls for nav
|
||||
//
|
||||
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
// Use flex for alignment (1-3)
|
||||
display: flex; // 1. allow flex styles
|
||||
align-items: center; // 2. vertically center contents
|
||||
justify-content: center; // 3. horizontally center contents
|
||||
width: $carousel-control-width;
|
||||
padding: 0;
|
||||
color: $carousel-control-color;
|
||||
text-align: center;
|
||||
background: none;
|
||||
filter: var(--#{$prefix}carousel-control-icon-filter);
|
||||
border: 0;
|
||||
opacity: $carousel-control-opacity;
|
||||
@include transition($carousel-control-transition);
|
||||
|
||||
// Hover/focus state
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: $carousel-control-color;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
opacity: $carousel-control-hover-opacity;
|
||||
}
|
||||
}
|
||||
.carousel-control-prev {
|
||||
left: 0;
|
||||
background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null);
|
||||
}
|
||||
.carousel-control-next {
|
||||
right: 0;
|
||||
background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null);
|
||||
}
|
||||
|
||||
// Icons for within
|
||||
.carousel-control-prev-icon,
|
||||
.carousel-control-next-icon {
|
||||
display: inline-block;
|
||||
width: $carousel-control-icon-width;
|
||||
height: $carousel-control-icon-width;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.carousel-control-prev-icon {
|
||||
background-image: escape-svg($carousel-control-prev-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-next-icon-bg) + "*/"};
|
||||
}
|
||||
.carousel-control-next-icon {
|
||||
background-image: escape-svg($carousel-control-next-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-prev-icon-bg) + "*/"};
|
||||
}
|
||||
|
||||
// Optional indicator pips/controls
|
||||
//
|
||||
// Add a container (such as a list) with the following class and add an item (ideally a focusable control,
|
||||
// like a button) with data-bs-target for each slide your carousel holds.
|
||||
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
// Use the .carousel-control's width as margin so we don't overlay those
|
||||
margin-right: $carousel-control-width;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: $carousel-control-width;
|
||||
|
||||
[data-bs-target] {
|
||||
box-sizing: content-box;
|
||||
flex: 0 1 auto;
|
||||
width: $carousel-indicator-width;
|
||||
height: $carousel-indicator-height;
|
||||
padding: 0;
|
||||
margin-right: $carousel-indicator-spacer;
|
||||
margin-left: $carousel-indicator-spacer;
|
||||
text-indent: -999px;
|
||||
cursor: pointer;
|
||||
background-color: var(--#{$prefix}carousel-indicator-active-bg);
|
||||
background-clip: padding-box;
|
||||
border: 0;
|
||||
// Use transparent borders to increase the hit area by 10px on top and bottom.
|
||||
border-top: $carousel-indicator-hit-area-height solid transparent;
|
||||
border-bottom: $carousel-indicator-hit-area-height solid transparent;
|
||||
opacity: $carousel-indicator-opacity;
|
||||
@include transition($carousel-indicator-transition);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: $carousel-indicator-active-opacity;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Optional captions
|
||||
//
|
||||
//
|
||||
|
||||
.carousel-caption {
|
||||
position: absolute;
|
||||
right: (100% - $carousel-caption-width) * .5;
|
||||
bottom: $carousel-caption-spacer;
|
||||
left: (100% - $carousel-caption-width) * .5;
|
||||
padding-top: $carousel-caption-padding-y;
|
||||
padding-bottom: $carousel-caption-padding-y;
|
||||
color: var(--#{$prefix}carousel-caption-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Dark mode carousel
|
||||
|
||||
@mixin carousel-dark() {
|
||||
--#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark};
|
||||
--#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark};
|
||||
--#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark};
|
||||
}
|
||||
|
||||
.carousel-dark {
|
||||
@include carousel-dark();
|
||||
}
|
||||
|
||||
:root,
|
||||
[data-bs-theme="light"] {
|
||||
--#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg};
|
||||
--#{$prefix}carousel-caption-color: #{$carousel-caption-color};
|
||||
--#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter};
|
||||
}
|
||||
|
||||
@if $enable-dark-mode {
|
||||
@include color-mode(dark, true) {
|
||||
@include carousel-dark();
|
||||
}
|
||||
}
|
||||
66
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_close.scss
vendored
Normal file
66
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_close.scss
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Transparent background and border properties included for button version.
|
||||
// iOS requires the button element instead of an anchor tag.
|
||||
// If you want the anchor version, it requires `href="#"`.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
|
||||
|
||||
.btn-close {
|
||||
// scss-docs-start close-css-vars
|
||||
--#{$prefix}btn-close-color: #{$btn-close-color};
|
||||
--#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) };
|
||||
--#{$prefix}btn-close-opacity: #{$btn-close-opacity};
|
||||
--#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity};
|
||||
--#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow};
|
||||
--#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity};
|
||||
--#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity};
|
||||
// scss-docs-end close-css-vars
|
||||
|
||||
box-sizing: content-box;
|
||||
width: $btn-close-width;
|
||||
height: $btn-close-height;
|
||||
padding: $btn-close-padding-y $btn-close-padding-x;
|
||||
color: var(--#{$prefix}btn-close-color);
|
||||
background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements
|
||||
filter: var(--#{$prefix}btn-close-filter);
|
||||
border: 0; // for button elements
|
||||
@include border-radius();
|
||||
opacity: var(--#{$prefix}btn-close-opacity);
|
||||
|
||||
// Override <a>'s hover style
|
||||
&:hover {
|
||||
color: var(--#{$prefix}btn-close-color);
|
||||
text-decoration: none;
|
||||
opacity: var(--#{$prefix}btn-close-hover-opacity);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: var(--#{$prefix}btn-close-focus-shadow);
|
||||
opacity: var(--#{$prefix}btn-close-focus-opacity);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
opacity: var(--#{$prefix}btn-close-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin btn-close-white() {
|
||||
--#{$prefix}btn-close-filter: #{$btn-close-filter-dark};
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
@include btn-close-white();
|
||||
}
|
||||
|
||||
:root,
|
||||
[data-bs-theme="light"] {
|
||||
--#{$prefix}btn-close-filter: #{$btn-close-filter};
|
||||
}
|
||||
|
||||
@if $enable-dark-mode {
|
||||
@include color-mode(dark, true) {
|
||||
@include btn-close-white();
|
||||
}
|
||||
}
|
||||
41
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_containers.scss
vendored
Normal file
41
extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_containers.scss
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Container widths
|
||||
//
|
||||
// Set the container width, and override it for fixed navbars in media queries.
|
||||
|
||||
@if $enable-container-classes {
|
||||
// Single container class with breakpoint max-widths
|
||||
.container,
|
||||
// 100% wide container at all breakpoints
|
||||
.container-fluid {
|
||||
@include make-container();
|
||||
}
|
||||
|
||||
// Responsive containers that are 100% wide until a breakpoint
|
||||
@each $breakpoint, $container-max-width in $container-max-widths {
|
||||
.container-#{$breakpoint} {
|
||||
@extend .container-fluid;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up($breakpoint, $grid-breakpoints) {
|
||||
%responsive-container-#{$breakpoint} {
|
||||
max-width: $container-max-width;
|
||||
}
|
||||
|
||||
// Extend each breakpoint which is smaller or equal to the current breakpoint
|
||||
$extend-breakpoint: true;
|
||||
|
||||
@each $name, $width in $grid-breakpoints {
|
||||
@if ($extend-breakpoint) {
|
||||
.container#{breakpoint-infix($name, $grid-breakpoints)} {
|
||||
@extend %responsive-container-#{$breakpoint};
|
||||
}
|
||||
|
||||
// Once the current breakpoint is reached, stop extending
|
||||
@if ($breakpoint == $name) {
|
||||
$extend-breakpoint: false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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