✨ (base): Añade componentes Nav y Navbar
Dropdown incorpora tamaño y estilo de botón configurables (`button::Size`/`Style`), reutilizados también por Navbar. El tema Basic sirve `basic.min.css` y añade el JS de inicialización de Navbar.
This commit is contained in:
parent
da959183f6
commit
35ae5bb6a8
22 changed files with 1695 additions and 280 deletions
14
CREDITS.md
14
CREDITS.md
|
|
@ -1,9 +1,9 @@
|
|||
# 🔃 Dependencias
|
||||
# Dependencias
|
||||
|
||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs/) como son:
|
||||
|
||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||
* [Axum](https://github.com/tokio-rs/axum) 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.
|
||||
|
|
@ -13,7 +13,7 @@ algunas de las librerías más robustas y populares del [ecosistema Rust](https:
|
|||
PageTop y sus extensiones.
|
||||
|
||||
|
||||
# 🗚 FIGfonts
|
||||
# FIGfonts
|
||||
|
||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
||||
|
|
@ -26,7 +26,13 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic
|
|||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
|
||||
|
||||
# 👾 Icono
|
||||
# Menús accesibles
|
||||
|
||||
El tema Basic de PageTop usa [Accessible Menu](https://github.com/NickDJM/accessible-menu),
|
||||
desarrollado por *Nick Milton*, para implementar la navegación de sus menús con soporte avanzado.
|
||||
|
||||
|
||||
# Icono
|
||||
|
||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
|
|
|
|||
202
Cargo.lock
generated
202
Cargo.lock
generated
|
|
@ -115,6 +115,18 @@ version = "1.0.102"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures 0.2.17",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
|
|
@ -242,6 +254,15 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -251,6 +272,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.12.1"
|
||||
|
|
@ -399,6 +429,12 @@ version = "0.9.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -424,6 +460,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.4.0"
|
||||
|
|
@ -501,6 +546,15 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
|
|
@ -541,7 +595,7 @@ version = "0.7.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"const-oid 0.9.6",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
@ -583,12 +637,23 @@ version = "0.10.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid 0.9.6",
|
||||
"crypto-common 0.1.7",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer 0.12.1",
|
||||
"const-oid 0.10.2",
|
||||
"crypto-common 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
|
@ -1053,7 +1118,7 @@ version = "0.12.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1116,6 +1181,15 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
|
|
@ -1489,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1716,6 +1790,7 @@ dependencies = [
|
|||
name = "pagetop"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"chrono",
|
||||
"colored",
|
||||
|
|
@ -1736,6 +1811,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tempfile",
|
||||
"terminal_size",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
|
|
@ -1745,6 +1821,17 @@ dependencies = [
|
|||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-admin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pagetop",
|
||||
"pagetop-seaorm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-aliner"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1777,6 +1864,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"pagetop",
|
||||
"pagetop-build",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1789,6 +1877,19 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-menu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"pagetop",
|
||||
"pagetop-seaorm",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-minimal"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1802,7 +1903,6 @@ dependencies = [
|
|||
name = "pagetop-seaorm"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"pagetop",
|
||||
"sea-orm",
|
||||
"sea-schema",
|
||||
|
|
@ -1820,6 +1920,24 @@ dependencies = [
|
|||
"path-slash 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pagetop-user"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64ct",
|
||||
"pagetop",
|
||||
"pagetop-admin",
|
||||
"pagetop-htmx",
|
||||
"pagetop-seaorm",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_qs",
|
||||
"sha2 0.11.0",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -1862,6 +1980,17 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.2"
|
||||
|
|
@ -2161,8 +2290,8 @@ version = "0.9.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
"const-oid 0.9.6",
|
||||
"digest 0.10.7",
|
||||
"num-bigint-dig",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
|
|
@ -2260,6 +2389,7 @@ checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957"
|
|||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"futures-util",
|
||||
"log",
|
||||
|
|
@ -2295,6 +2425,7 @@ version = "0.32.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"inherent",
|
||||
"ordered-float",
|
||||
"sea-query-derive",
|
||||
|
|
@ -2306,6 +2437,7 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"sea-query",
|
||||
"sqlx",
|
||||
]
|
||||
|
|
@ -2436,6 +2568,18 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
|
|
@ -2464,8 +2608,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2475,8 +2619,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2510,7 +2665,7 @@ version = "2.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
|
|
@ -2591,6 +2746,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
|
|
@ -2609,7 +2765,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
@ -2646,7 +2802,7 @@ dependencies = [
|
|||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
"sqlx-core",
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
|
|
@ -2667,8 +2823,9 @@ dependencies = [
|
|||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"digest",
|
||||
"digest 0.10.7",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"futures-channel",
|
||||
|
|
@ -2689,7 +2846,7 @@ dependencies = [
|
|||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
|
|
@ -2708,6 +2865,7 @@ dependencies = [
|
|||
"base64",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
"dotenvy",
|
||||
"etcetera",
|
||||
|
|
@ -2726,7 +2884,7 @@ dependencies = [
|
|||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sha2 0.10.9",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
|
|
@ -2742,6 +2900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
"flume",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
|
@ -3044,9 +3203,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.11"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
|
|
@ -3065,6 +3224,7 @@ dependencies = [
|
|||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
18
Cargo.toml
18
Cargo.toml
|
|
@ -10,7 +10,10 @@ members = [
|
|||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
"extensions/pagetop-htmx",
|
||||
"extensions/pagetop-admin",
|
||||
"extensions/pagetop-menu",
|
||||
"extensions/pagetop-seaorm",
|
||||
"extensions/pagetop-user",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
@ -21,8 +24,10 @@ license = "MIT OR Apache-2.0"
|
|||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
argon2 = "0.5"
|
||||
async-trait = "0.1"
|
||||
axum = { version = "0.8" }
|
||||
base64ct = { version = "1.8", features = ["alloc"] }
|
||||
change-detection = "1.2"
|
||||
chrono = "0.4"
|
||||
colored = "3.1"
|
||||
|
|
@ -43,14 +48,18 @@ path-slash = "0.2"
|
|||
proc-macro2 = "1.0"
|
||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
||||
quote = "1.0"
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_qs = "1.1"
|
||||
sha2 = "0.11"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
tempfile = "3.27"
|
||||
terminal_size = "0.4"
|
||||
thiserror = "2.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["fs"] }
|
||||
tower-http = { version = "0.7", features = ["catch-panic", "fs"] }
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||
|
|
@ -62,16 +71,19 @@ pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" }
|
|||
pagetop-minimal = { version = "0.1", path = "helpers/pagetop-minimal" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-admin = { version = "0.1", path = "extensions/pagetop-admin" }
|
||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
||||
pagetop-htmx = { version = "0.1", path = "extensions/pagetop-htmx" }
|
||||
pagetop-menu = { version = "0.1", path = "extensions/pagetop-menu" }
|
||||
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||
pagetop-user = { version = "0.1", path = "extensions/pagetop-user" }
|
||||
# PageTop
|
||||
pagetop = { version = "0.5", path = "." }
|
||||
|
||||
[workspace.dependencies.sea-orm]
|
||||
version = "1.1"
|
||||
features = ["debug-print", "macros", "runtime-tokio-native-tls"]
|
||||
features = ["debug-print", "macros", "runtime-tokio-native-tls", "with-chrono"]
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.sea-schema]
|
||||
|
|
@ -99,6 +111,7 @@ default = []
|
|||
testing = []
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum.workspace = true
|
||||
chrono.workspace = true
|
||||
colored.workspace = true
|
||||
|
|
@ -114,6 +127,7 @@ pagetop-minimal.workspace = true
|
|||
pagetop-statics.workspace = true
|
||||
serde.workspace = true
|
||||
terminal_size.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
|
|
|
|||
|
|
@ -44,21 +44,10 @@ body {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
* Brand component
|
||||
*/
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--val-color--text);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand .image {
|
||||
/* Header, Aside, Content y Footer se apilan en el mismo orden ocupando todo el ancho disponible. */
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -87,6 +76,70 @@ body {
|
|||
.badge-warning { background-color: var(--val-color--warning); }
|
||||
.badge-severe { background-color: var(--val-color--danger); }
|
||||
|
||||
/*
|
||||
* Brand component
|
||||
*/
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--val-color--text);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.brand .image {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/*
|
||||
* Breadcrumb component
|
||||
*/
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.breadcrumb-item + .breadcrumb-item {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
float: left;
|
||||
padding-right: 0.5rem;
|
||||
color: var(--val-color--text--muted);
|
||||
content: "/";
|
||||
}
|
||||
.breadcrumb-item.active {
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
.breadcrumb-item a {
|
||||
color: var(--val-color--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Geometría base de .dropdown-button/.dropdown-toggle (ver "Dropdown component" más abajo). Se
|
||||
* declara aquí, antes de "Buttons", para que las clases .button-sm/.button-lg/.button-* ganen el
|
||||
* empate de especificidad frente a ella cuando se combinan en el mismo elemento (CSS resuelve un
|
||||
* empate por orden de aparición en el fichero, no por el orden de las clases en el HTML). */
|
||||
.dropdown-button,
|
||||
.dropdown-toggle {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font: inherit;
|
||||
color: var(--val-color--text);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--val-color--border);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
* Buttons
|
||||
*/
|
||||
|
|
@ -194,6 +247,171 @@ body {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Dialog component
|
||||
*/
|
||||
|
||||
.dialog {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
color: var(--val-color--text);
|
||||
background-color: var(--val-color--bg);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
color: var(--val-color--text--muted);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dialog-close::before {
|
||||
content: "\2715";
|
||||
}
|
||||
.dialog-close:hover {
|
||||
color: var(--val-color--text);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
}
|
||||
|
||||
/*
|
||||
* Dropdown component
|
||||
*/
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* La geometría base de .dropdown-button/.dropdown-toggle vive junto a "Buttons" (antes de esta
|
||||
* sección), para que las clases .button-sm/.button-lg/.button-* ganen el empate de especificidad
|
||||
* frente a ella (CSS resuelve un empate por orden de aparición en el fichero, no por el orden de
|
||||
* las clases en el HTML). */
|
||||
.dropdown-toggle::after {
|
||||
content: "\25BC";
|
||||
display: inline-block;
|
||||
margin-left: 0.35em;
|
||||
font-size: 0.8em;
|
||||
transition: transform .15s ease-in-out;
|
||||
}
|
||||
.dropdown:has(> .dropdown-menu.show) > .dropdown-toggle::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 12rem;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0.5rem 0;
|
||||
list-style: none;
|
||||
background-color: var(--val-color--bg);
|
||||
border: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
/* Closed by default */
|
||||
display: none;
|
||||
}
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 1rem;
|
||||
text-align: left;
|
||||
color: var(--val-color--text);
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
|
||||
}
|
||||
.dropdown-item.active {
|
||||
color: var(--val-color--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.dropdown-item.disabled {
|
||||
color: var(--val-color--text--muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-item-text {
|
||||
display: block;
|
||||
padding: 0.375rem 1rem;
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem 0.25rem;
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--val-color--border);
|
||||
}
|
||||
|
||||
/* Utility: visually hidden but still accessible to assistive technology */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Form components
|
||||
*/
|
||||
|
|
@ -293,175 +511,20 @@ input:disabled + label {
|
|||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
/*
|
||||
* Dialog component
|
||||
*/
|
||||
|
||||
.dialog {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
color: var(--val-color--text);
|
||||
background-color: var(--val-color--bg);
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.dialog::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
color: var(--val-color--text--muted);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dialog-close::before {
|
||||
content: "\2715";
|
||||
}
|
||||
.dialog-close:hover {
|
||||
color: var(--val-color--text);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
}
|
||||
|
||||
/*
|
||||
* Dropdown component
|
||||
*/
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-button,
|
||||
.dropdown-toggle {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font: inherit;
|
||||
color: var(--val-color--text);
|
||||
background-color: transparent;
|
||||
.form-field > select.form-select,
|
||||
.form-field > input.form-control,
|
||||
.form-field > textarea.form-control {
|
||||
border: 1px solid var(--val-color--border);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
content: "\25BC";
|
||||
display: inline-block;
|
||||
margin-left: 0.35em;
|
||||
font-size: 0.8em;
|
||||
transition: transform .15s ease-in-out;
|
||||
}
|
||||
.dropdown:has(> .dropdown-menu.show) > .dropdown-toggle::after {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 12rem;
|
||||
margin: 0.25rem 0 0;
|
||||
padding: 0.5rem 0;
|
||||
list-style: none;
|
||||
background-color: var(--val-color--bg);
|
||||
border: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
/* Closed by default */
|
||||
display: none;
|
||||
}
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 1rem;
|
||||
text-align: left;
|
||||
color: var(--val-color--text);
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
|
||||
}
|
||||
.dropdown-item.active {
|
||||
color: var(--val-color--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.dropdown-item.disabled {
|
||||
color: var(--val-color--text--muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-item-text {
|
||||
display: block;
|
||||
padding: 0.375rem 1rem;
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
display: block;
|
||||
padding: 0.5rem 1rem 0.25rem;
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 0;
|
||||
margin: 0.5rem 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--val-color--border);
|
||||
}
|
||||
|
||||
/* Utility: visually hidden but still accessible to assistive technology */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
.form-field > select.form-select:focus,
|
||||
.form-field > input.form-control:focus,
|
||||
.form-field > textarea.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--val-color--primary);
|
||||
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--val-color--primary) 25%, transparent);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -514,59 +577,123 @@ input:disabled + label {
|
|||
}
|
||||
|
||||
/*
|
||||
* Table component
|
||||
* Nav component
|
||||
*/
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
.nav-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
color: var(--val-color--text);
|
||||
text-decoration: none;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom-width: 2px;
|
||||
.nav-link:hover {
|
||||
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
|
||||
}
|
||||
.nav-link.active {
|
||||
color: var(--val-color--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-sort {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.table-sort:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.table-sort::after {
|
||||
content: "\25BC";
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.table-sort-asc::after {
|
||||
content: "\25B2";
|
||||
font-size: 0.8em;
|
||||
opacity: 1;
|
||||
}
|
||||
.table-sort-desc::after {
|
||||
content: "\25BC";
|
||||
font-size: 0.8em;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
.nav-link.disabled {
|
||||
color: var(--val-color--text--muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.nav-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.nav-vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.nav-fill > .nav-item {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.nav-justified > .nav-item {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* Navbar component
|
||||
*/
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
display: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--val-color--border);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navbar-toggle::before {
|
||||
content: "\2630";
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.navbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.navbar:has(.navbar-toggle) .navbar-toggle {
|
||||
display: inline-block;
|
||||
order: -1;
|
||||
}
|
||||
.navbar:has(.navbar-toggle) .navbar-content {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.navbar:has(.navbar-toggle) .navbar-content.show {
|
||||
display: flex;
|
||||
}
|
||||
.navbar .nav {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -686,7 +813,7 @@ input:disabled + label {
|
|||
border: 0;
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
background-color: var(--val-color--light);
|
||||
background-color: var(--val-color--bg--muted);
|
||||
}
|
||||
.pager-jump-input > input.form-control {
|
||||
text-align: center;
|
||||
|
|
@ -709,36 +836,60 @@ input:disabled + label {
|
|||
}
|
||||
|
||||
/*
|
||||
* Breadcrumb component
|
||||
* Table component
|
||||
*/
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.breadcrumb-item + .breadcrumb-item {
|
||||
padding-left: 0.5rem;
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
float: left;
|
||||
padding-right: 0.5rem;
|
||||
color: var(--val-color--text--muted);
|
||||
content: "/";
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg));
|
||||
}
|
||||
.breadcrumb-item.active {
|
||||
color: var(--val-color--text--muted);
|
||||
|
||||
.table thead th {
|
||||
border-bottom-width: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.breadcrumb-item a {
|
||||
color: var(--val-color--text);
|
||||
|
||||
.table-sort {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb-item a:hover {
|
||||
.table-sort:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.table-sort::after {
|
||||
content: "\25BC";
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.15;
|
||||
}
|
||||
.table-sort-asc::after {
|
||||
content: "\25B2";
|
||||
font-size: 0.8em;
|
||||
opacity: 1;
|
||||
}
|
||||
.table-sort-desc::after {
|
||||
content: "\25BC";
|
||||
font-size: 0.8em;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
/*
|
||||
* Region Footer
|
||||
|
|
@ -748,3 +899,76 @@ input:disabled + label {
|
|||
padding: .75rem 0 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* pagetop-user: login page
|
||||
*/
|
||||
|
||||
.user-login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 70vh;
|
||||
padding: 2rem 1rem;
|
||||
background: radial-gradient(
|
||||
ellipse 120% 60% at 50% -10%,
|
||||
color-mix(in srgb, var(--val-color--primary) 18%, var(--val-color--bg)) 0%,
|
||||
var(--val-color--bg) 70%
|
||||
);
|
||||
}
|
||||
|
||||
.user-login-card {
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
padding: 2rem;
|
||||
background-color: #fff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.user-login-card .form-label {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.user-login-card .button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.user-admin-table-wrapper {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.user-admin-table {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.user-admin-permission-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.user-admin-permission-granted {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-admin-permission-missing {
|
||||
color: var(--val-color--text--muted);
|
||||
}
|
||||
|
||||
.user-form-error {
|
||||
padding: 0.625rem 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #fff;
|
||||
background-color: var(--val-color--danger);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.user-register-link,
|
||||
.user-reset-link {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ body {
|
|||
|
||||
.intro {
|
||||
position: relative;
|
||||
min-width: 350px;
|
||||
color: var(--intro-color);
|
||||
background-color: var(--intro-bg-color);
|
||||
|
||||
|
|
|
|||
18
assets/js/basic.navbar.init.js
Normal file
18
assets/js/basic.navbar.init.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Conecta cada botón `.navbar-toggle` con el contenido que controla (`aria-controls`), alternando
|
||||
// `aria-expanded` en el botón y la clase `.show` en el contenido. A diferencia de `Dropdown` no usa
|
||||
// `accessible-menu`: el contenido de una barra de navegación no es una única lista de elementos
|
||||
// (puede llevar varios `Nav`, texto, lo que sea), así que no encaja con el modelo de menú de esa
|
||||
// librería (ver `base/component/navbar/`).
|
||||
document.querySelectorAll(".navbar-toggle").forEach(function (toggle) {
|
||||
var id = toggle.getAttribute("aria-controls");
|
||||
var content = id && document.getElementById(id);
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", function () {
|
||||
var expanded = toggle.getAttribute("aria-expanded") === "true";
|
||||
toggle.setAttribute("aria-expanded", String(!expanded));
|
||||
content.classList.toggle("show", !expanded);
|
||||
});
|
||||
});
|
||||
17
build.rs
17
build.rs
|
|
@ -1,4 +1,4 @@
|
|||
use pagetop_build::{StaticFilesBundle, copy_dir};
|
||||
use pagetop_build::{StaticFilesBundle, compile_scss, copy_dir, minify_js};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
// Regenera `static/` desde cero sólo si hay cambios en `assets/`.
|
||||
|
|
@ -7,6 +7,21 @@ fn main() -> std::io::Result<()> {
|
|||
|
||||
copy_dir("assets", "static")?;
|
||||
|
||||
// CSS: genera la variante minificada de `basic.css` del tema Basic.
|
||||
compile_scss("assets/css/basic.css", "static/css/basic.min.css")?;
|
||||
|
||||
// JS: minifica el manejo de `Dialog` del tema Basic.
|
||||
minify_js(
|
||||
"assets/js/basic.dialog.init.js",
|
||||
"static/js/basic.dialog.min.js",
|
||||
)?;
|
||||
|
||||
// JS: minifica la integración de `Dropdown` con `accessible-menu` del tema Basic.
|
||||
minify_js(
|
||||
"assets/js/basic.dropdown.init.js",
|
||||
"static/js/basic.dropdown.min.js",
|
||||
)?;
|
||||
|
||||
StaticFilesBundle::from_dir("./static", None)
|
||||
.with_name("assets")
|
||||
.build()
|
||||
|
|
|
|||
46
examples/basic-nav.rs
Normal file
46
examples/basic-nav.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
struct NavDemo;
|
||||
|
||||
#[async_trait]
|
||||
impl Extension for NavDemo {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![&pagetop::base::extension::Welcome]
|
||||
}
|
||||
|
||||
async fn initialize(&self) {
|
||||
let main_nav = Nav::new()
|
||||
.with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
.with_item(nav::Item::link_blank(
|
||||
Lc::n("Docs"),
|
||||
"https://docs.rs/pagetop",
|
||||
))
|
||||
.with_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(Lc::n("Tools"))
|
||||
.with_item(dropdown::Item::link(Lc::n("Generator"), "/tools/gen"))
|
||||
.with_item(dropdown::Item::link(Lc::n("Reports"), "/tools/reports"))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::header(Lc::n("Danger zone")))
|
||||
.with_item(dropdown::Item::button(Lc::n("Reset settings"))),
|
||||
))
|
||||
.with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#"));
|
||||
|
||||
let user_menu = Dropdown::new()
|
||||
.with_title(Lc::n("Account"))
|
||||
.with_item(dropdown::Item::label(Lc::n("Signed in as demo")))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::link(Lc::n("Profile"), "/profile"))
|
||||
.with_item(dropdown::Item::link(Lc::n("Settings"), "/settings"))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::button(Lc::n("Sign out")));
|
||||
|
||||
InRegion::Global(&CoreRegions::Header).add(main_nav);
|
||||
InRegion::Global(&CoreRegions::Aside).add(user_menu);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&NavDemo).await.run().await
|
||||
}
|
||||
55
examples/basic-navbar.rs
Normal file
55
examples/basic-navbar.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct NavbarDemo;
|
||||
|
||||
#[async_trait]
|
||||
impl Extension for NavbarDemo {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
&pagetop_bootsier::Bootsier,
|
||||
&pagetop::base::extension::Welcome,
|
||||
]
|
||||
}
|
||||
|
||||
async fn initialize(&self) {
|
||||
let brand = Brand::new()
|
||||
.with_image(image::Source::logo(PageTopSvg::Color))
|
||||
.with_title(Lc::n("PageTop"))
|
||||
.with_route(Route::from("/"));
|
||||
|
||||
let main_nav = Nav::new()
|
||||
.with_item(nav::Item::link(Lc::t("navbar_home", &LOC), "/"))
|
||||
.with_item(nav::Item::link_blank(
|
||||
Lc::t("navbar_docs", &LOC),
|
||||
"https://docs.rs/pagetop",
|
||||
))
|
||||
.with_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(Lc::t("navbar_tools", &LOC))
|
||||
.with_item(dropdown::Item::link(
|
||||
Lc::t("navbar_generator", &LOC),
|
||||
"/tools/gen",
|
||||
))
|
||||
.with_item(dropdown::Item::link(
|
||||
Lc::t("navbar_reports", &LOC),
|
||||
"/tools/reports",
|
||||
)),
|
||||
))
|
||||
.with_item(nav::Item::link(Lc::t("navbar_active", &LOC), "/active").with_active(true))
|
||||
.with_item(nav::Item::link_disabled(
|
||||
Lc::t("navbar_disabled", &LOC),
|
||||
"#",
|
||||
));
|
||||
|
||||
let navbar = Navbar::brand_left(brand).with_item(navbar::Item::nav(main_nav));
|
||||
|
||||
InRegion::Global(&CoreRegions::Header).add(navbar);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&NavbarDemo).await.run().await
|
||||
}
|
||||
7
examples/locale/en-US/basic-navbar.ftl
Normal file
7
examples/locale/en-US/basic-navbar.ftl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
navbar_home = Home
|
||||
navbar_docs = Docs
|
||||
navbar_tools = Tools
|
||||
navbar_generator = Generator
|
||||
navbar_reports = Reports
|
||||
navbar_active = Active page
|
||||
navbar_disabled = Disabled
|
||||
7
examples/locale/es-ES/basic-navbar.ftl
Normal file
7
examples/locale/es-ES/basic-navbar.ftl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
navbar_home = Inicio
|
||||
navbar_docs = Documentación
|
||||
navbar_tools = Herramientas
|
||||
navbar_generator = Generador
|
||||
navbar_reports = Informes
|
||||
navbar_active = Página activa
|
||||
navbar_disabled = Deshabilitado
|
||||
|
|
@ -47,6 +47,14 @@ pub use intro::{Intro, IntroOpening};
|
|||
mod messages;
|
||||
pub use messages::Messages;
|
||||
|
||||
pub mod nav;
|
||||
#[doc(inline)]
|
||||
pub use nav::Nav;
|
||||
|
||||
pub mod navbar;
|
||||
#[doc(inline)]
|
||||
pub use navbar::Navbar;
|
||||
|
||||
mod pager;
|
||||
pub use pager::{Pager, PagerAlign, PagerVisibility};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ pub struct Dropdown {
|
|||
title: Lc,
|
||||
/// Devuelve si el botón se desdobla (*split*) en botón de acción más *toggle*.
|
||||
button_split: bool,
|
||||
/// Devuelve el tamaño visual del botón.
|
||||
#[getters(copy)]
|
||||
button_size: button::Size,
|
||||
/// Devuelve el estilo visual del botón.
|
||||
#[getters(copy)]
|
||||
button_style: button::Style,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
|
@ -65,15 +71,44 @@ impl Component for Dropdown {
|
|||
});
|
||||
}
|
||||
|
||||
// Sin tamaño ni estilo (caso más común), evita construir dinámicamente las clases.
|
||||
let (button_classes, toggle_classes): (CowStr, CowStr) =
|
||||
if matches!(self.button_size(), button::Size::None)
|
||||
&& matches!(self.button_style(), button::Style::None)
|
||||
{
|
||||
(
|
||||
CowStr::from("dropdown-button"),
|
||||
CowStr::from("dropdown-toggle"),
|
||||
)
|
||||
} else {
|
||||
use button::{Size, Style};
|
||||
|
||||
let size_class = match self.button_size() {
|
||||
Size::None => "",
|
||||
Size::Small => " button-sm",
|
||||
Size::Large => " button-lg",
|
||||
};
|
||||
let style_class = match self.button_style() {
|
||||
Style::None => String::new(),
|
||||
Style::Solid(intent) => util::join!(" button-", intent.color(cx)),
|
||||
Style::Outline(intent) => util::join!(" button-outline-", intent.color(cx)),
|
||||
Style::Link => " button-link".to_string(),
|
||||
};
|
||||
(
|
||||
util::join!("dropdown-button", size_class, &style_class).into(),
|
||||
util::join!("dropdown-toggle", size_class, &style_class).into(),
|
||||
)
|
||||
};
|
||||
|
||||
let toggle_label = Lc::l("dropdown_toggle").using(cx);
|
||||
|
||||
Ok(html! {
|
||||
div (self.props()) {
|
||||
@if *self.button_split() {
|
||||
button type="button" class="dropdown-button" { (&title) }
|
||||
button type="button" class=(&button_classes) { (&title) }
|
||||
button
|
||||
type="button"
|
||||
class="dropdown-toggle"
|
||||
class=(&toggle_classes)
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
|
|
@ -82,7 +117,7 @@ impl Component for Dropdown {
|
|||
} @else {
|
||||
button
|
||||
type="button"
|
||||
class="dropdown-toggle"
|
||||
class=(&toggle_classes)
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
|
|
@ -126,6 +161,20 @@ impl Dropdown {
|
|||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño visual del botón (usa [`button::Size::None`] para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_button_size(mut self, size: button::Size) -> Self {
|
||||
self.button_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el estilo visual del botón (usa [`button::Style::None`] para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_button_style(mut self, style: button::Style) -> Self {
|
||||
self.button_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`TypedOp`].
|
||||
///
|
||||
|
|
|
|||
10
src/base/component/nav.rs
Normal file
10
src/base/component/nav.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! Definiciones para crear menús de navegación planos ([`Nav`]) y sus elementos ([`Item`]).
|
||||
|
||||
mod props;
|
||||
pub use props::Layout;
|
||||
|
||||
mod component;
|
||||
pub use component::Nav;
|
||||
|
||||
mod item;
|
||||
pub use item::{Item, ItemKind};
|
||||
113
src/base/component/nav/component.rs
Normal file
113
src/base/component/nav/component.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear un **menú de navegación plano**.
|
||||
///
|
||||
/// Presenta una lista de elementos [`nav::Item`](super::Item), donde alguno puede desplegar un
|
||||
/// [`Dropdown`](super::super::Dropdown) embebido.
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let nav = nav::Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link_blank(Lc::n("External"), "https://docs.rs"))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// dropdown::Dropdown::new()
|
||||
/// .with_title(Lc::n("Options"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Action"), "/action"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Another"), "/another")),
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Nav {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve la distribución y orientación seleccionada.
|
||||
nav_layout: nav::Layout,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Nav {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() {
|
||||
nav::Layout::Default => "nav",
|
||||
nav::Layout::Start => "nav nav-start",
|
||||
nav::Layout::Center => "nav nav-center",
|
||||
nav::Layout::End => "nav nav-end",
|
||||
nav::Layout::Vertical => "nav nav-vertical",
|
||||
nav::Layout::Fill => "nav nav-fill",
|
||||
nav::Layout::Justified => "nav nav-justified",
|
||||
}));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let items = self.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
Ok(html! {
|
||||
ul (self.props()) {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Nav {
|
||||
// **< Nav BUILDER >****************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Selecciona la distribución y orientación del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
||||
self.nav_layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una
|
||||
/// operación [`TypedOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// nav.with_item(nav::Item::link("Inicio", "/"));
|
||||
/// nav.with_item(TypedOp::AddMany(vec![
|
||||
/// nav::Item::link(...),
|
||||
/// nav::Item::link_disabled(...),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, op: impl Into<TypedOp<nav::Item>>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
291
src/base/component/nav/item.rs
Normal file
291
src/base/component/nav/item.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
||||
/// Tipos de [`nav::Item`](super::Item) disponibles en un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(Lc),
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica resuelta por una [`Route`].
|
||||
/// Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente deshabilitado.
|
||||
Link {
|
||||
label: Lc,
|
||||
route: Route,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
||||
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
||||
Html(Embed<Html>),
|
||||
/// Elemento que despliega un menú [`Dropdown`](super::super::Dropdown).
|
||||
Dropdown(Embed<Dropdown>),
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
const ITEM: &str = "nav-item";
|
||||
const DROPDOWN: &str = "nav-item dropdown";
|
||||
|
||||
/// Devuelve las clases base asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Void => "",
|
||||
Self::Dropdown(_) => Self::DROPDOWN,
|
||||
_ => Self::ITEM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Representa un **elemento individual** de un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Cada instancia de [`nav::Item`](super::Item) se traduce en un componente visible que puede
|
||||
/// comportarse como texto, enlace, contenido HTML o menú desplegable, según su [`ItemKind`].
|
||||
///
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
/// Devuelve el valor de `active` forzado explícitamente, si lo hay.
|
||||
///
|
||||
/// Sin forzar (`None`), un [`ItemKind::Link`] se marca activo cuando su ruta coincide
|
||||
/// exactamente con la del *request* actual (ver [`prepare()`](Component::prepare)). Forzarlo
|
||||
/// permite otros criterios, como marcar activa una sección mientras la ruta actual sea
|
||||
/// cualquiera de sus subrutas.
|
||||
active_override: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => html! {
|
||||
li (self.props()) {
|
||||
span class="nav-link disabled" aria-disabled="true" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let enabled = !*disabled;
|
||||
|
||||
// Deshabilitado, `href` no la usa: evita resolver la ruta para nada.
|
||||
let route_link = enabled.then(|| route.resolve(cx));
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = self
|
||||
.active_override()
|
||||
.copied()
|
||||
.unwrap_or(enabled && route_link.as_ref().map(RoutePath::path) == current_path);
|
||||
|
||||
let active_class = if is_current { " active" } else { "" };
|
||||
let disabled_class = if *disabled { " disabled" } else { "" };
|
||||
let classes = util::join!("nav-link", active_class, disabled_class);
|
||||
|
||||
let target = (enabled && *blank).then_some("_blank");
|
||||
let rel = (enabled && *blank).then_some("noopener noreferrer");
|
||||
let aria_current = (enabled && is_current).then_some("page");
|
||||
let aria_disabled = (*disabled).then_some("true");
|
||||
|
||||
html! {
|
||||
li (self.props()) {
|
||||
a
|
||||
class=(classes)
|
||||
href=[route_link]
|
||||
target=[target]
|
||||
rel=[rel]
|
||||
aria-current=[aria_current]
|
||||
aria-disabled=[aria_disabled]
|
||||
{
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Html(html) => html! {
|
||||
li (self.props()) {
|
||||
(html.render(cx).await)
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.get() {
|
||||
let items = dd.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let title = dd.title().using(cx);
|
||||
let title = if title.is_empty() {
|
||||
Lc::l("dropdown_default_title").using(cx)
|
||||
} else {
|
||||
title
|
||||
};
|
||||
html! {
|
||||
li (self.props()) {
|
||||
a
|
||||
class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
(title)
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: Lc) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
///
|
||||
/// La ruta se obtiene invocando [`Route::resolve()`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta
|
||||
/// actual del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
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: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento con contenido HTML arbitrario.
|
||||
///
|
||||
/// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `<li>`
|
||||
/// con las clases de navegación asociadas a [`Item`].
|
||||
pub fn html(html: Html) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Html(Embed::with(html)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento de navegación que contiene un menú desplegable
|
||||
/// [`Dropdown`](super::super::Dropdown).
|
||||
///
|
||||
/// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la
|
||||
/// lista de elementos** del [`Dropdown`](super::super::Dropdown); el resto de propiedades no
|
||||
/// afectarán a su representación en [`Nav`](super::Nav).
|
||||
pub fn dropdown(menu: Dropdown) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Dropdown(Embed::with(menu)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Fuerza si un [`ItemKind::Link`] se marca activo, o `None` para volver a la detección
|
||||
/// automática por coincidencia exacta de ruta.
|
||||
#[builder_fn]
|
||||
pub fn with_active(mut self, active: impl Into<Option<bool>>) -> Self {
|
||||
self.active_override = active.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
26
src/base/component/nav/props.rs
Normal file
26
src/base/component/nav/props.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Distribución y orientación de un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Las variantes son puramente semánticas: cada tema decide en su propio `setup()` qué clases CSS
|
||||
/// les corresponden, según su propio vocabulario de estilos.
|
||||
#[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,
|
||||
}
|
||||
10
src/base/component/navbar.rs
Normal file
10
src/base/component/navbar.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! Definiciones para crear barras de navegación ([`Navbar`]) y sus elementos ([`Item`]).
|
||||
|
||||
mod props;
|
||||
pub use props::Layout;
|
||||
|
||||
mod component;
|
||||
pub use component::Navbar;
|
||||
|
||||
mod item;
|
||||
pub use item::Item;
|
||||
238
src/base/component/navbar/component.rs
Normal file
238
src/base/component/navbar/component.rs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear una **barra de navegación**.
|
||||
///
|
||||
/// Permite mostrar enlaces, menús desplegables ([`nav::Item::dropdown()`]) y una marca de
|
||||
/// identidad, en distintas disposiciones controladas por [`navbar::Layout`].
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// Barra **simple**, sólo con un menú horizontal:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let navbar = Navbar::simple()
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link(Lc::n("About"), "/about"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Contact"), "/contact")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra **colapsable**, con botón de despliegue:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let navbar = Navbar::simple_toggle()
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link_blank(Lc::n("Doc"), "https://docs.rs"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Support"), "/support")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **marca de identidad** y menú, con menús desplegables:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let brand = Brand::new()
|
||||
/// .with_title(Lc::n("PageTop"))
|
||||
/// .with_route(Route::from("/"));
|
||||
///
|
||||
/// let navbar = Navbar::brand_left(brand)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// Dropdown::new()
|
||||
/// .with_title(Lc::n("Tools"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Generator"), "/tools/gen"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Reports"), "/tools/reports")),
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **botón de despliegue** y **marca de identidad**, en ese orden:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let brand = Brand::new()
|
||||
/// .with_title(Lc::n("Intranet"))
|
||||
/// .with_route(Route::from("/"));
|
||||
///
|
||||
/// let navbar = Navbar::brand_right(brand).with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Dashboard"), "/dashboard"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Users"), "/users")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// [`nav::Item::dropdown()`]: super::super::nav::Item::dropdown
|
||||
/// [`navbar::Layout`]: super::Layout
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Navbar {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
layout: navbar::Layout,
|
||||
/// Devuelve la lista de contenidos.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, cx: &Context) {
|
||||
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
|
||||
// despliegue para referenciar el contenido colapsable con `aria-controls`.
|
||||
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
|
||||
self.alter_prop(PropsOp::prepend_classes("navbar"));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Botón de despliegue para el contenido colapsable de la barra.
|
||||
fn button(cx: &mut Context, id_content: &str) -> Markup {
|
||||
html! {
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggle"
|
||||
aria-expanded="false"
|
||||
aria-controls=(id_content)
|
||||
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
|
||||
{
|
||||
span class="navbar-toggle-icon" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||
let items = self.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// `setup()` garantiza que habrá un `id` antes de renderizar.
|
||||
let id = self.id().unwrap();
|
||||
let id_content = util::join!(id, "-content");
|
||||
|
||||
Ok(html! {
|
||||
nav (self.props()) {
|
||||
@match self.layout() {
|
||||
// Barra más sencilla: sólo contenido, siempre visible.
|
||||
navbar::Layout::Simple => {
|
||||
div class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca, siempre visible, sin botón.
|
||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
div class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca y botón, en ese orden.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con botón y marca, en ese orden.
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
(brand.render(cx).await)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Navbar {
|
||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
||||
pub fn simple() -> Self {
|
||||
Self::default().with_layout(navbar::Layout::Simple)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **simple pero colapsable**, con botón de despliegue.
|
||||
pub fn simple_toggle() -> Self {
|
||||
Self::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **con marca de identidad**, siempre visible, sin botón.
|
||||
pub fn simple_brand_left(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca de identidad** y **botón de despliegue**, en ese
|
||||
/// orden.
|
||||
pub fn brand_left(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **botón de despliegue** y **marca de identidad**, en ese
|
||||
/// orden.
|
||||
pub fn brand_right(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
||||
}
|
||||
|
||||
// **< Navbar BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
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
|
||||
}
|
||||
|
||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||
/// barra con una operación [`TypedOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// navbar.with_item(navbar::Item::nav(...));
|
||||
/// navbar.with_item(TypedOp::AddMany(vec![
|
||||
/// navbar::Item::nav(...),
|
||||
/// navbar::Item::text(...),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, op: impl Into<TypedOp<navbar::Item>>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
84
src/base/component/navbar/item.rs
Normal file
84
src/base/component/navbar/item.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Elementos que puede contener una barra de navegación [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
||||
/// contenido** de la barra (la parte colapsable), por lo que son independientes de la marca o del
|
||||
/// botón que ya pueda definir el propio [`navbar::Layout`](super::Layout).
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Item {
|
||||
/// Sin contenido, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
|
||||
///
|
||||
/// Útil cuando el [`navbar::Layout`](super::Layout) no incluye marca, y se quiere incluir
|
||||
/// dentro del área colapsable. Si el *layout* ya muestra una marca, esta variante no la
|
||||
/// sustituye, sólo añade otra dentro del bloque de contenidos.
|
||||
Brand(Embed<Brand>),
|
||||
/// Representa un menú de navegación [`Nav`].
|
||||
Nav(Embed<Nav>),
|
||||
/// Representa un *texto localizado* libre.
|
||||
Text(Lc),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Void => None,
|
||||
Self::Brand(brand) => brand.id(),
|
||||
Self::Nav(nav) => nav.id(),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if let Self::Nav(nav) = self
|
||||
&& let Some(nav) = nav.get_mut()
|
||||
{
|
||||
// Se añade aquí, antes de que `nav.render(cx)` (en `prepare()`) dispare el ciclo de
|
||||
// vida normal del `Nav` embebido y su propio `setup()` prepend las clases de `nav`/
|
||||
// `nav::Layout`.
|
||||
nav.alter_prop(PropsOp::prepend_classes("navbar-nav"));
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self {
|
||||
Self::Void => html! {},
|
||||
Self::Brand(brand) => html! { (brand.render(cx).await) },
|
||||
Self::Nav(nav) => html! { (nav.render(cx).await) },
|
||||
Self::Text(text) => html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo [`navbar::Brand`](super::super::Brand) para añadir en el contenido
|
||||
/// de [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Pensado para barras colapsables donde se quiere que la marca aparezca en la zona
|
||||
/// desplegable.
|
||||
pub fn brand(brand: Brand) -> Self {
|
||||
Self::Brand(Embed::with(brand))
|
||||
}
|
||||
|
||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`](super::Navbar).
|
||||
pub fn nav(item: Nav) -> Self {
|
||||
Self::Nav(Embed::with(item))
|
||||
}
|
||||
|
||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||
pub fn text(item: Lc) -> Self {
|
||||
Self::Text(item)
|
||||
}
|
||||
}
|
||||
32
src/base/component/navbar/props.rs
Normal file
32
src/base/component/navbar/props.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa las distintas formas de presentación de una barra de navegación
|
||||
/// [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Sólo recoge las combinaciones de marca y botón de despliegue independientes de cualquier
|
||||
/// framework CSS. Un tema puede definir su propia variante de disposición (posiciones fijas,
|
||||
/// contenido en un panel lateral...) con su propio tipo, sin depender de éste.
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
#[default]
|
||||
Simple,
|
||||
|
||||
/// Barra simple, con botón de despliegue y sin marca de identidad.
|
||||
SimpleToggle,
|
||||
|
||||
/// Barra simple, con marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
SimpleBrandLeft(Embed<Brand>),
|
||||
|
||||
/// Barra con marca de identidad y botón de despliegue, en ese orden.
|
||||
BrandLeft(Embed<Brand>),
|
||||
|
||||
/// Barra con botón de despliegue y marca de identidad, en ese orden.
|
||||
BrandRight(Embed<Brand>),
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ impl Theme for Basic {
|
|||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/pagetop/css/basic.css")
|
||||
StyleSheet::from("/pagetop/css/basic.min.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
|
|
@ -30,6 +30,9 @@ impl Theme for Basic {
|
|||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/pagetop/js/basic.dropdown.min.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/pagetop/js/basic.navbar.init.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/pagetop/js/basic.dialog.min.js").with_version(PAGETOP_VERSION),
|
||||
))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue