Compare commits
172 commits
main
...
pagetop-wo
| Author | SHA1 | Date | |
|---|---|---|---|
| f819181872 | |||
| 806ed9e0dc | |||
| f337119219 | |||
| 2299e63585 | |||
| 70744a51a5 | |||
| 35f2b66161 | |||
| c848b97450 | |||
| b5da11777a | |||
| 56a2471090 | |||
| c06efbd0d1 | |||
| 04502ef81e | |||
| 4cd75af8d2 | |||
| 90e4f85bd4 | |||
| 3df73f0551 | |||
| 5d199ef10d | |||
| dbdaeb6151 | |||
| 93cd3ba6c5 | |||
| 9e2fc2ccfa | |||
| 165cb49880 | |||
| f265f217bd | |||
| 3f3634fdab | |||
| 58d1fef4f1 | |||
| 5b2ddeb6cc | |||
| 58b4b4e036 | |||
| 1254e650f1 | |||
| 11c2bdffb1 | |||
| 6df71885cc | |||
| c47724bb68 | |||
| f8f023b513 | |||
| 16eec316c4 | |||
| 404739a3fd | |||
| 8651b91ab2 | |||
| c6cd4fcd7d | |||
| 9143f1eb26 | |||
| 5bf28ef4f0 | |||
| 307ca6f62c | |||
| dfff12881f | |||
| c2ec7568f0 | |||
| 3c3ffc91e6 | |||
| 50e4b42fe4 | |||
| 64113aff09 | |||
| 0b8f3f3000 | |||
| 8577ca8a59 | |||
| ee1b48ff3f | |||
| 28f1eee391 | |||
| 1e2d171805 | |||
| f2a7507317 | |||
| b163b7726e | |||
| 5939d33964 | |||
| ab1d4f0efb | |||
| a2b3ae2eb8 | |||
| 17e16652e4 | |||
| 4e4fdf7b10 | |||
| 0d38f2e9e7 | |||
| 1be3d88888 | |||
| c34dc02357 | |||
| bea4b3f30d | |||
| def0513246 | |||
| 043873a954 | |||
| 88acc08b5f | |||
| c0a5a8c3ab | |||
| eb63a7ef37 | |||
| 35ae5bb6a8 | |||
| da959183f6 | |||
| 434d31c5eb | |||
| 56a6691c87 | |||
| af53cd4b52 | |||
| 76c5cb9b76 | |||
| 3c67d9d04c | |||
| 99a4837e11 | |||
| c8654a5742 | |||
| d8f82ea1d2 | |||
| e24b9ab9de | |||
| 4ba66f5995 | |||
| 997017bb75 | |||
| b50d186fb1 | |||
| bb6855e41f | |||
| 1453764462 | |||
| fe2b2c176f | |||
| bf4c635e59 | |||
| 213aa18b12 | |||
| c3ff8a6ff8 | |||
| 745d492d5b | |||
| 05187c9343 | |||
| ed30b2ae17 | |||
| 946d335664 | |||
| e5431bc7d7 | |||
| 51e8b11090 | |||
| 3930499664 | |||
| 5842ea6efc | |||
| 17a9f0c9e0 | |||
| aa8c4100b0 | |||
| be10dd28a1 | |||
| 3198b74399 | |||
| 6bea4a5793 | |||
| 20b6c77c9e | |||
| 8cb5e035dc | |||
| 825283caa3 | |||
| ee57d6cacf | |||
| 12c14afbf6 | |||
| 3b01894024 | |||
| e8bba0803d | |||
| 502c00c891 | |||
| 1cab0ae9a0 | |||
| c49bf8f56a | |||
| 545a87fbb0 | |||
| 4cc72aac0e | |||
| 3e83f8e35e | |||
| fdd2dca280 | |||
| 1a3a295f18 | |||
| c91b1cd35c | |||
| 8ddc26dae1 | |||
| dc2ed27da4 | |||
| 747e64ce34 | |||
| ea0dc021ce | |||
| 9db1d4bab0 | |||
| 74dbcde5c5 | |||
| 7e6a9f8557 | |||
| 845d91d8c1 | |||
| ce6b4d2581 | |||
| 65c8a788c2 | |||
| 39f494539e | |||
| b9d9cdf601 | |||
| 8573aca29e | |||
| e7f2563967 | |||
| 55159f6d8f | |||
| af548b03c9 | |||
| 6f82da220b | |||
| 370bc8ee47 | |||
| e6b4661f21 | |||
| 5a8267032e | |||
| f6f78fddbe | |||
| 7a3419415e | |||
| 82e64238bd | |||
| eb3157c4a4 | |||
| 30151b6254 | |||
| 24bd13414c | |||
| 0e72d51276 | |||
| 771f61bf29 | |||
| 654618aa68 | |||
| 87ad78778d | |||
| bff7cf1b38 | |||
| 9e8cd77140 | |||
| 25713a8a13 | |||
| a26aa4d30a | |||
| c7b680a7f7 | |||
| 9b2d430d8b | |||
| 1139df6210 | |||
| 9354894b3a | |||
| 9acd8cc51a | |||
| aa8b109668 | |||
| 8aa4372bdf | |||
| 54d5b3e53d | |||
| 0e96bcce64 | |||
| 3120fd9a6f | |||
| ee6457acf6 | |||
| f37db56ec5 | |||
| 64db114eb0 | |||
| 9435678e01 | |||
| 26f1cda831 | |||
| d495c05b96 | |||
| dbf0894894 | |||
| 2202a2350c | |||
| 62219584b0 | |||
| 8d0103c257 | |||
| 1bd97d5705 | |||
| f27790c3a2 | |||
| 6376e3e88c | |||
| 38fd24453e | |||
| 511149caa7 | |||
| 41c4379bc3 | |||
| f9e87058d8 |
3
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
# Ignora directorios de compilación
|
# Ignora directorios de compilación
|
||||||
**/target
|
**/target
|
||||||
|
|
||||||
|
# Ignora directorios de archivos estáticos
|
||||||
|
**/static
|
||||||
|
|
||||||
# Archivos de log
|
# Archivos de log
|
||||||
**/log/*.log*
|
**/log/*.log*
|
||||||
|
|
||||||
|
|
|
||||||
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
|
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:
|
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.
|
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||||
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
* [Tracing](https://github.com/tokio-rs/tracing) para la gestión de trazas y registro de eventos
|
||||||
de la aplicación.
|
de la aplicación.
|
||||||
|
|
@ -13,7 +13,7 @@ algunas de las librerías más robustas y populares del [ecosistema Rust](https:
|
||||||
PageTop y sus extensiones.
|
PageTop y sus extensiones.
|
||||||
|
|
||||||
|
|
||||||
# 🗚 FIGfonts
|
# FIGfonts
|
||||||
|
|
||||||
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan*
|
||||||
para mostrar un banner de presentación en el terminal con el nombre de la aplicación en caracteres
|
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*
|
* [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).
|
"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
|
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||||
|
|
|
||||||
291
Cargo.lock
generated
|
|
@ -20,6 +20,15 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -106,6 +115,18 @@ version = "1.0.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
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]]
|
[[package]]
|
||||||
name = "async-stream"
|
name = "async-stream"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
|
@ -233,6 +254,15 @@ dependencies = [
|
||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
|
|
@ -242,6 +272,15 @@ dependencies = [
|
||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bstr"
|
name = "bstr"
|
||||||
version = "1.12.1"
|
version = "1.12.1"
|
||||||
|
|
@ -309,6 +348,16 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"phf 0.12.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
|
|
@ -357,12 +406,6 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "concat-string"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -390,6 +433,12 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -415,6 +464,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc"
|
name = "crc"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
|
|
@ -492,6 +550,15 @@ dependencies = [
|
||||||
"typenum",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
|
|
@ -532,7 +599,7 @@ version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"pem-rfc7468",
|
"pem-rfc7468",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -574,12 +641,23 @@ version = "0.10.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer 0.10.4",
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"crypto-common",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|
@ -933,7 +1011,7 @@ version = "0.4.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.1.4",
|
||||||
"bstr",
|
"bstr",
|
||||||
"log",
|
"log",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
|
|
@ -961,10 +1039,20 @@ dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"lasso",
|
"lasso",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"phf",
|
"phf 0.11.3",
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bumpalo",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
|
@ -1034,7 +1122,7 @@ version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1097,6 +1185,15 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
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]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
|
@ -1470,7 +1567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1495,6 +1592,17 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minify-js"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1fa5546ee8bd66024113e506cabe4230e76635a094c06ea2051b66021dda92e"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick 0.7.20",
|
||||||
|
"lazy_static",
|
||||||
|
"parse-js",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -1686,8 +1794,10 @@ dependencies = [
|
||||||
name = "pagetop"
|
name = "pagetop"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
"colored",
|
"colored",
|
||||||
"config",
|
"config",
|
||||||
"figlet-rs",
|
"figlet-rs",
|
||||||
|
|
@ -1706,6 +1816,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|
@ -1715,13 +1826,23 @@ dependencies = [
|
||||||
"unic-langid",
|
"unic-langid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pagetop-admin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"pagetop",
|
||||||
|
"pagetop-seaorm",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pagetop-aliner"
|
name = "pagetop-aliner"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pagetop",
|
"pagetop",
|
||||||
"pagetop-build",
|
"pagetop-build",
|
||||||
"tokio",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1731,7 +1852,6 @@ dependencies = [
|
||||||
"pagetop",
|
"pagetop",
|
||||||
"pagetop-build",
|
"pagetop-build",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1739,9 +1859,19 @@ name = "pagetop-build"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"grass",
|
"grass",
|
||||||
|
"minify-js",
|
||||||
"pagetop-statics",
|
"pagetop-statics",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pagetop-htmx"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"pagetop",
|
||||||
|
"pagetop-build",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pagetop-macros"
|
name = "pagetop-macros"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1756,7 +1886,6 @@ dependencies = [
|
||||||
name = "pagetop-minimal"
|
name = "pagetop-minimal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concat-string",
|
|
||||||
"indoc",
|
"indoc",
|
||||||
"pastey",
|
"pastey",
|
||||||
]
|
]
|
||||||
|
|
@ -1765,7 +1894,6 @@ dependencies = [
|
||||||
name = "pagetop-seaorm"
|
name = "pagetop-seaorm"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
|
||||||
"pagetop",
|
"pagetop",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-schema",
|
"sea-schema",
|
||||||
|
|
@ -1783,6 +1911,24 @@ dependencies = [
|
||||||
"path-slash 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -1812,6 +1958,30 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-js"
|
||||||
|
version = "0.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2742b5e32dcb5930447ed9f9e401a7dfd883867fc079c4fac44ae8ba3593710e"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick 0.7.20",
|
||||||
|
"bumpalo",
|
||||||
|
"hashbrown 0.13.2",
|
||||||
|
"lazy_static",
|
||||||
|
"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]]
|
[[package]]
|
||||||
name = "pastey"
|
name = "pastey"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -1867,7 +2037,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_macros",
|
"phf_macros",
|
||||||
"phf_shared",
|
"phf_shared 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared 0.12.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1876,7 +2055,7 @@ version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_shared",
|
"phf_shared 0.11.3",
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1887,7 +2066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_generator",
|
"phf_generator",
|
||||||
"phf_shared",
|
"phf_shared 0.11.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|
@ -1902,6 +2081,15 @@ dependencies = [
|
||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -2094,7 +2282,7 @@ version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.1.4",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
@ -2111,8 +2299,8 @@ version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid",
|
"const-oid 0.9.6",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
"num-bigint-dig",
|
"num-bigint-dig",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|
@ -2210,6 +2398,7 @@ checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -2245,6 +2434,7 @@ version = "0.32.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c"
|
checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"inherent",
|
"inherent",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
"sea-query-derive",
|
"sea-query-derive",
|
||||||
|
|
@ -2256,6 +2446,7 @@ version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608"
|
checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
]
|
]
|
||||||
|
|
@ -2386,6 +2577,18 @@ dependencies = [
|
||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
@ -2414,8 +2617,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2425,8 +2628,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
|
|
@ -2460,7 +2674,7 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2541,6 +2755,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
|
|
@ -2559,7 +2774,7 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -2596,7 +2811,7 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-mysql",
|
"sqlx-mysql",
|
||||||
"sqlx-postgres",
|
"sqlx-postgres",
|
||||||
|
|
@ -2617,8 +2832,9 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"digest",
|
"digest 0.10.7",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
|
@ -2639,7 +2855,7 @@ dependencies = [
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
|
|
@ -2658,6 +2874,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
|
|
@ -2676,7 +2893,7 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2 0.10.9",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
|
|
@ -2692,6 +2909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
|
"chrono",
|
||||||
"flume",
|
"flume",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -2994,9 +3212,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-http"
|
name = "tower-http"
|
||||||
version = "0.6.11"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -3015,6 +3233,7 @@ dependencies = [
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
26
Cargo.toml
|
|
@ -9,7 +9,10 @@ members = [
|
||||||
# Extensions
|
# Extensions
|
||||||
"extensions/pagetop-aliner",
|
"extensions/pagetop-aliner",
|
||||||
"extensions/pagetop-bootsier",
|
"extensions/pagetop-bootsier",
|
||||||
|
"extensions/pagetop-htmx",
|
||||||
|
"extensions/pagetop-admin",
|
||||||
"extensions/pagetop-seaorm",
|
"extensions/pagetop-seaorm",
|
||||||
|
"extensions/pagetop-user",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
@ -20,12 +23,14 @@ license = "MIT OR Apache-2.0"
|
||||||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
argon2 = "0.5"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
axum = { version = "0.8" }
|
axum = { version = "0.8" }
|
||||||
|
base64ct = { version = "1.8", features = ["alloc"] }
|
||||||
change-detection = "1.2"
|
change-detection = "1.2"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
chrono-tz = "0.10"
|
||||||
colored = "3.1"
|
colored = "3.1"
|
||||||
concat-string = "1.0"
|
|
||||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||||
figlet-rs = "1.0"
|
figlet-rs = "1.0"
|
||||||
fluent-templates = "0.14"
|
fluent-templates = "0.14"
|
||||||
|
|
@ -35,20 +40,25 @@ indexmap = "2.14"
|
||||||
indoc = "2.0"
|
indoc = "2.0"
|
||||||
itoa = "1.0"
|
itoa = "1.0"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
|
minify-js = "0.6"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
pastey = "0.2"
|
pastey = "0.2"
|
||||||
path-slash = "0.2"
|
path-slash = "0.2"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
proc-macro2-diagnostics = { version = "0.10", default-features = false }
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
|
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_qs = "1.1"
|
||||||
|
sha2 = "0.11"
|
||||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||||
tempfile = "3.27"
|
tempfile = "3.27"
|
||||||
terminal_size = "0.4"
|
terminal_size = "0.4"
|
||||||
|
thiserror = "2.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
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 = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||||
|
|
@ -60,15 +70,18 @@ pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" }
|
||||||
pagetop-minimal = { version = "0.1", path = "helpers/pagetop-minimal" }
|
pagetop-minimal = { version = "0.1", path = "helpers/pagetop-minimal" }
|
||||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||||
# Extensions
|
# Extensions
|
||||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
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-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
||||||
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
pagetop-htmx = { version = "0.1", path = "extensions/pagetop-htmx" }
|
||||||
|
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||||
|
pagetop-user = { version = "0.1", path = "extensions/pagetop-user" }
|
||||||
# PageTop
|
# PageTop
|
||||||
pagetop = { version = "0.5", path = "." }
|
pagetop = { version = "0.5", path = "." }
|
||||||
|
|
||||||
[workspace.dependencies.sea-orm]
|
[workspace.dependencies.sea-orm]
|
||||||
version = "1.1"
|
version = "1.1"
|
||||||
features = ["debug-print", "macros", "runtime-tokio-native-tls"]
|
features = ["debug-print", "macros", "runtime-tokio-native-tls", "with-chrono"]
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.sea-schema]
|
[workspace.dependencies.sea-schema]
|
||||||
|
|
@ -96,8 +109,10 @@ default = []
|
||||||
testing = []
|
testing = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait.workspace = true
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
chrono-tz.workspace = true
|
||||||
colored.workspace = true
|
colored.workspace = true
|
||||||
config.workspace = true
|
config.workspace = true
|
||||||
figlet-rs.workspace = true
|
figlet-rs.workspace = true
|
||||||
|
|
@ -111,6 +126,7 @@ pagetop-minimal.workspace = true
|
||||||
pagetop-statics.workspace = true
|
pagetop-statics.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
terminal_size.workspace = true
|
terminal_size.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tower.workspace = true
|
tower.workspace = true
|
||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
|
|
|
||||||
40
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/banner.png" />
|
<img src="https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/banner.png" />
|
||||||
|
|
||||||
<h1>PageTop</h1>
|
<h1>PageTop</h1>
|
||||||
|
|
||||||
|
|
@ -11,9 +11,8 @@
|
||||||
[](https://crates.io/crates/pagetop)
|
[](https://crates.io/crates/pagetop)
|
||||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
||||||
|
|
@ -29,8 +28,7 @@ según las necesidades de cada proyecto, incluyendo:
|
||||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||||
componentes.
|
componentes.
|
||||||
|
|
||||||
|
## Guía rápida
|
||||||
## ⚡️ Guía rápida
|
|
||||||
|
|
||||||
La aplicación más sencilla de PageTop se ve así:
|
La aplicación más sencilla de PageTop se ve así:
|
||||||
|
|
||||||
|
|
@ -39,43 +37,43 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::new().run()?.await
|
Application::new().await.run().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Este código arranca el servidor de PageTop. Con la configuración por defecto, muestra una página de
|
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`.
|
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:
|
Para personalizar el servicio, puedes crear una extensión de PageTop de la siguiente manera:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
scfg.route("/", service::web::get().to(hello_world));
|
router.route("/", web::get(hello_world))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
.with_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&HelloWorld).run()?.await
|
Application::prepare(&HelloWorld).await.run().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Este programa implementa una extensión llamada `HelloWorld` que sirve una página web en la ruta raíz
|
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>`.
|
(`/`) mostrando el texto "Hello World!" dentro de un elemento HTML `<h1>`.
|
||||||
|
|
||||||
|
## Proyecto
|
||||||
## 📂 Proyecto
|
|
||||||
|
|
||||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||||
|
|
||||||
|
|
@ -113,8 +111,7 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
||||||
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
|
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
|
||||||
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
|
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
|
||||||
|
|
||||||
|
## Pruebas
|
||||||
## 🧪 Pruebas
|
|
||||||
|
|
||||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
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:
|
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||||
|
|
@ -134,15 +131,13 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
||||||
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
|
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
|
||||||
> `cargo test`.
|
> `cargo test`.
|
||||||
|
|
||||||
|
## Advertencia
|
||||||
## 🚧 Advertencia
|
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
**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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
## Licencia
|
||||||
## 📜 Licencia
|
|
||||||
|
|
||||||
El código está disponible bajo una doble licencia:
|
El código está disponible bajo una doble licencia:
|
||||||
|
|
||||||
|
|
@ -155,8 +150,7 @@ El código está disponible bajo una doble licencia:
|
||||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||||
el ecosistema Rust.
|
el ecosistema Rust.
|
||||||
|
|
||||||
|
## Contribuir
|
||||||
## ✨ Contribuir
|
|
||||||
|
|
||||||
PageTop mantiene **un único repositorio oficial**:
|
PageTop mantiene **un único repositorio oficial**:
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
45
assets/js/basic.dialog.init.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Abre y cierra los componentes `Dialog` de Basic (elemento `<dialog>` nativo) interpretando el
|
||||||
|
// vocabulario de atributos `data-dialog-*`, común a todos los temas: Bootsier traduce estos mismos
|
||||||
|
// nombres a los suyos propios (`data-bs-*`) en `setup()` para que el JS de Bootstrap los reconozca,
|
||||||
|
// así el marcado de un disparador o de un botón de cierre es idéntico en los dos temas. Sin
|
||||||
|
// dependencias: ni htmx, ni Bootstrap, ni ninguna otra librería.
|
||||||
|
|
||||||
|
// Abre el diálogo referenciado por `data-dialog-target` al pulsar un elemento con
|
||||||
|
// `data-dialog-toggle="modal"`.
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
var toggle = event.target.closest('[data-dialog-toggle="modal"]');
|
||||||
|
if (!toggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var target = toggle.getAttribute("data-dialog-target");
|
||||||
|
var dialog = target && document.querySelector(target);
|
||||||
|
if (!(dialog instanceof HTMLDialogElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
dialog.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cierra el `<dialog>` ascendente más cercano al pulsar un elemento con
|
||||||
|
// `data-dialog-dismiss="modal"`.
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
var dismiss = event.target.closest('[data-dialog-dismiss="modal"]');
|
||||||
|
if (!dismiss) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dialog = dismiss.closest("dialog");
|
||||||
|
if (dialog) {
|
||||||
|
event.preventDefault();
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cierra el diálogo al pulsar sobre el fondo (`::backdrop`): un clic ahí llega con el propio
|
||||||
|
// `<dialog>` como `event.target`, porque el fondo no forma parte de su caja de contenido.
|
||||||
|
document.querySelectorAll("dialog.dialog").forEach(function (dialog) {
|
||||||
|
dialog.addEventListener("click", function (event) {
|
||||||
|
if (event.target === dialog) {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
66
assets/js/basic.dropdown.init.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Conecta cada `Dropdown` (standalone o embebido en un `Nav`) con `accessible-menu` (ver
|
||||||
|
// basic.menu.min.js), que añade los roles ARIA, el `aria-expanded` y la navegación por teclado
|
||||||
|
// sobre el marcado ya generado en el servidor. Selecciona por clase, no por etiqueta, porque el
|
||||||
|
// disparador es un `<button>` en un `Dropdown` independiente y un `<a>` cuando cuelga de un
|
||||||
|
// `nav::Item::dropdown()`.
|
||||||
|
document.querySelectorAll(".dropdown > .dropdown-toggle").forEach(function (toggle) {
|
||||||
|
var container = toggle.parentElement;
|
||||||
|
var menu = container.querySelector(":scope > .dropdown-menu");
|
||||||
|
if (!menu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new TopLinkDisclosureMenu({
|
||||||
|
menuElement: menu,
|
||||||
|
containerElement: container,
|
||||||
|
controllerElement: toggle,
|
||||||
|
// `Header`/`Divider`/`Label` no son interactivos: se excluyen de `menuItemSelector`
|
||||||
|
// (si no, la librería asume que todo `<li>` tiene un enlace y lanza un TypeError al
|
||||||
|
// intentar añadirle listeners de foco).
|
||||||
|
menuItemSelector: "li:has(> a.dropdown-item, > button.dropdown-item)",
|
||||||
|
menuLinkSelector: "a.dropdown-item, button.dropdown-item",
|
||||||
|
// La navegación con flechas/Home/End es opcional en la librería (`false` por defecto);
|
||||||
|
// se activa para igualar el comportamiento habitual de un menú desplegable.
|
||||||
|
optionalKeySupport: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cada `TopLinkDisclosureMenu` es independiente: al hacer clic fuera sólo cierra su propio menú
|
||||||
|
// (ver `_handleClick()` en accessible-menu), así que abrir un desplegable no cierra otro que ya
|
||||||
|
// estuviera abierto en otra parte de la página. Se corrige aquí escuchando `accessibleMenuExpand`
|
||||||
|
// -evento que la librería emite en cada apertura- y cerrando el resto de menús de nivel superior
|
||||||
|
// ya registrados, vía `window.AccessibleMenu.menus`, el registro global que la propia librería
|
||||||
|
// mantiene con todas las instancias (incluye las de `basic.menu.init.js`).
|
||||||
|
document.addEventListener("accessibleMenuExpand", function (event) {
|
||||||
|
var openedToggle = event.detail.toggle;
|
||||||
|
var menus = (window.AccessibleMenu && window.AccessibleMenu.menus) || {};
|
||||||
|
Object.values(menus).forEach(function (menu) {
|
||||||
|
var controller = menu.elements.controller;
|
||||||
|
if (controller && controller !== openedToggle) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// El cierre "al hacer clic fuera" que trae la librería no funciona para un `controllerElement` de
|
||||||
|
// nivel superior como el nuestro: en `toggleToggle()` (accessible-menu), al abrir, hace
|
||||||
|
// `menu.focusState = "self"` y a continuación `toggle.elements.controlledMenu.focusState =
|
||||||
|
// "none"` -pensado para cuando el toggle abre un submenú hijo distinto del menú que lo contiene-,
|
||||||
|
// pero aquí `menu` y `toggle.elements.controlledMenu` son el mismo objeto: la segunda asignación
|
||||||
|
// deshace la primera. El cierre nativo de la librería depende de `focusState !== "none"`, así que
|
||||||
|
// nunca se dispara aunque el menú esté abierto (`isOpen` sí queda correcto). Se sustituye aquí por
|
||||||
|
// una comprobación propia basada sólo en `isOpen` y en la posición del clic.
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
var menus = (window.AccessibleMenu && window.AccessibleMenu.menus) || {};
|
||||||
|
Object.values(menus).forEach(function (menu) {
|
||||||
|
var controller = menu.elements.controller;
|
||||||
|
if (
|
||||||
|
controller &&
|
||||||
|
controller.isOpen &&
|
||||||
|
!menu.dom.menu.contains(event.target) &&
|
||||||
|
!controller.dom.toggle.contains(event.target)
|
||||||
|
) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
3
assets/js/basic.menu.min.js
vendored
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
41
assets/scss/basic.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// PageTop Basic theme styles.
|
||||||
|
//
|
||||||
|
// Compiled by `build.rs` into `static/css/basic.min.css`. Comments written with `//` never reach
|
||||||
|
// the compiled file, so they can be as long as needed.
|
||||||
|
//
|
||||||
|
// The partials are emitted in the order they are loaded here, which matters when two rules tie on
|
||||||
|
// specificity: the later one wins.
|
||||||
|
|
||||||
|
// Third-party reset (normalize.css v8.0.1, MIT), copied verbatim. It goes first so that every
|
||||||
|
// rule of the theme can override it.
|
||||||
|
@use "basic/normalize";
|
||||||
|
|
||||||
|
// Custom properties and base styles.
|
||||||
|
@use "basic/root";
|
||||||
|
@use "basic/base";
|
||||||
|
|
||||||
|
// Components, in alphabetical order. `buttons` must go before `dropdown` and `nav`, see the notes
|
||||||
|
// in `basic/_buttons.scss`.
|
||||||
|
@use "basic/badge";
|
||||||
|
@use "basic/brand";
|
||||||
|
@use "basic/breadcrumb";
|
||||||
|
@use "basic/buttons";
|
||||||
|
@use "basic/container";
|
||||||
|
@use "basic/dialog";
|
||||||
|
@use "basic/dropdown";
|
||||||
|
@use "basic/form";
|
||||||
|
@use "basic/image";
|
||||||
|
@use "basic/messages";
|
||||||
|
@use "basic/nav";
|
||||||
|
@use "basic/navbar";
|
||||||
|
@use "basic/pager";
|
||||||
|
@use "basic/table";
|
||||||
|
|
||||||
|
// Theme regions.
|
||||||
|
@use "basic/regions";
|
||||||
|
|
||||||
|
// Styles for the pages of the `pagetop-user` extension.
|
||||||
|
@use "basic/user";
|
||||||
|
|
||||||
|
// Utilities go last so they win over any component rule they are combined with.
|
||||||
|
@use "basic/utilities";
|
||||||
21
assets/scss/basic/_badge.scss
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.3em 0.6em;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--val-color--secondary);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge-info,
|
||||||
|
.badge-warning {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.badge-primary { background-color: var(--val-color--primary); }
|
||||||
|
.badge-neutral { background-color: var(--val-color--secondary); }
|
||||||
|
.badge-success { background-color: var(--val-color--success); }
|
||||||
|
.badge-info { background-color: var(--val-color--info); }
|
||||||
|
.badge-warning { background-color: var(--val-color--warning); }
|
||||||
|
.badge-severe { background-color: var(--val-color--danger); }
|
||||||
23
assets/scss/basic/_base.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--val-font-family);
|
||||||
|
font-size: var(--val-fs--base);
|
||||||
|
font-weight: var(--val-fw--base);
|
||||||
|
line-height: var(--val-lh--base);
|
||||||
|
color: var(--val-color--text);
|
||||||
|
background-color: var(--val-color--bg);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header, Aside, Content and Footer are stacked in that order, using the full available width.
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
12
assets/scss/basic/_brand.scss
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
27
assets/scss/basic/_breadcrumb.scss
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
25
assets/scss/basic/_breakpoints.scss
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Breakpoints of the Basic theme, used to generate the responsive classes of `Container` and
|
||||||
|
// `Navbar`. They match the default `Theme::breakpoint_entry()` table (`xs` has no minimum width, so
|
||||||
|
// it is left out), so a child theme that renames or resizes the breakpoints must also redefine the
|
||||||
|
// classes generated from them.
|
||||||
|
//
|
||||||
|
// This partial emits no CSS on its own.
|
||||||
|
|
||||||
|
$breakpoints: (
|
||||||
|
sm: 576px,
|
||||||
|
md: 768px,
|
||||||
|
lg: 992px,
|
||||||
|
xl: 1200px,
|
||||||
|
xxl: 1400px,
|
||||||
|
xxxl: 1920px,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Maximum width of `.container` and `.container-{name}` from each breakpoint on.
|
||||||
|
$container-max-widths: (
|
||||||
|
sm: 540px,
|
||||||
|
md: 720px,
|
||||||
|
lg: 960px,
|
||||||
|
xl: 1140px,
|
||||||
|
xxl: 1320px,
|
||||||
|
xxxl: 1800px,
|
||||||
|
);
|
||||||
120
assets/scss/basic/_buttons.scss
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// Base geometry of .dropdown-button/.dropdown-toggle (the rest is in `_dropdown.scss`). It is
|
||||||
|
// declared here, before the button classes, so that .button-sm/.button-lg/.button-* win the
|
||||||
|
// specificity tie against it when combined on the same element (CSS resolves a tie by order of
|
||||||
|
// appearance in the file, not by the order of the classes in the 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: var(--val-fs--base);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: var(--val-lh--base);
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--val-color--primary);
|
||||||
|
border: 1px solid var(--val-color--primary);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .15s ease-in-out, border-color .15s ease-in-out;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||||
|
border-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||||
|
}
|
||||||
|
.button:disabled,
|
||||||
|
.button.disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.button::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-sm {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.button-lg {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary,
|
||||||
|
.button-neutral,
|
||||||
|
.button-success,
|
||||||
|
.button-severe {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.button-info,
|
||||||
|
.button-warning {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.button-primary { background-color: var(--val-color--primary); border-color: var(--val-color--primary); }
|
||||||
|
.button-neutral { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); }
|
||||||
|
.button-success { background-color: var(--val-color--success); border-color: var(--val-color--success); }
|
||||||
|
.button-info { background-color: var(--val-color--info); border-color: var(--val-color--info); }
|
||||||
|
.button-warning { background-color: var(--val-color--warning); border-color: var(--val-color--warning); }
|
||||||
|
.button-severe { background-color: var(--val-color--danger); border-color: var(--val-color--danger); }
|
||||||
|
|
||||||
|
.button-primary:hover { background-color: color-mix(in srgb, var(--val-color--primary) 85%, black); border-color: color-mix(in srgb, var(--val-color--primary) 85%, black); }
|
||||||
|
.button-neutral:hover { background-color: color-mix(in srgb, var(--val-color--secondary) 85%, black); border-color: color-mix(in srgb, var(--val-color--secondary) 85%, black); }
|
||||||
|
.button-success:hover { background-color: color-mix(in srgb, var(--val-color--success) 85%, black); border-color: color-mix(in srgb, var(--val-color--success) 85%, black); }
|
||||||
|
.button-info:hover { background-color: color-mix(in srgb, var(--val-color--info) 85%, black); border-color: color-mix(in srgb, var(--val-color--info) 85%, black); }
|
||||||
|
.button-warning:hover { background-color: color-mix(in srgb, var(--val-color--warning) 85%, black); border-color: color-mix(in srgb, var(--val-color--warning) 85%, black); }
|
||||||
|
.button-severe:hover { background-color: color-mix(in srgb, var(--val-color--danger) 85%, black); border-color: color-mix(in srgb, var(--val-color--danger) 85%, black); }
|
||||||
|
|
||||||
|
.button-outline-primary,
|
||||||
|
.button-outline-neutral,
|
||||||
|
.button-outline-success,
|
||||||
|
.button-outline-info,
|
||||||
|
.button-outline-warning,
|
||||||
|
.button-outline-severe {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.button-outline-primary { border-color: var(--val-color--primary); color: var(--val-color--primary); }
|
||||||
|
.button-outline-neutral { border-color: var(--val-color--secondary); color: var(--val-color--secondary); }
|
||||||
|
.button-outline-success { border-color: var(--val-color--success); color: var(--val-color--success); }
|
||||||
|
.button-outline-info { border-color: var(--val-color--info); color: var(--val-color--info); }
|
||||||
|
.button-outline-warning { border-color: var(--val-color--warning); color: var(--val-color--warning); }
|
||||||
|
.button-outline-severe { border-color: var(--val-color--danger); color: var(--val-color--danger); }
|
||||||
|
|
||||||
|
.button-outline-primary:hover { background-color: var(--val-color--primary); border-color: var(--val-color--primary); color: #fff; }
|
||||||
|
.button-outline-neutral:hover { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); color: #fff; }
|
||||||
|
.button-outline-success:hover { background-color: var(--val-color--success); border-color: var(--val-color--success); color: #fff; }
|
||||||
|
.button-outline-info:hover { background-color: var(--val-color--info); border-color: var(--val-color--info); color: #000; }
|
||||||
|
.button-outline-warning:hover { background-color: var(--val-color--warning); border-color: var(--val-color--warning); color: #000; }
|
||||||
|
.button-outline-severe:hover { background-color: var(--val-color--danger); border-color: var(--val-color--danger); color: #fff; }
|
||||||
|
|
||||||
|
.button-link {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--val-color--primary);
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.button-link:hover {
|
||||||
|
color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.button-link:disabled,
|
||||||
|
.button-link.disabled {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-set {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
32
assets/scss/basic/_container.scss
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
@use "sass:list";
|
||||||
|
@use "sass:map";
|
||||||
|
@use "breakpoints" as bp;
|
||||||
|
|
||||||
|
// Classes added by the `Container` component from `Width`. All of them are centered and fill the
|
||||||
|
// available width; `.container` and `.container-{name}` also get a maximum width from the
|
||||||
|
// breakpoint they name (`.container` from the first one).
|
||||||
|
|
||||||
|
$named: ();
|
||||||
|
@each $name in map.keys(bp.$breakpoints) {
|
||||||
|
$named: list.append($named, ".container-#{$name}", comma);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container,
|
||||||
|
.container-fluid,
|
||||||
|
#{$named} {
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From each breakpoint on, `.container` and every `.container-{name}` up to that breakpoint take
|
||||||
|
// its maximum width, while the larger ones stay fluid until their own breakpoint is reached.
|
||||||
|
$sized: (".container");
|
||||||
|
@each $name, $width in bp.$breakpoints {
|
||||||
|
$sized: list.append($sized, ".container-#{$name}", comma);
|
||||||
|
@media (width >= $width) {
|
||||||
|
#{$sized} {
|
||||||
|
max-width: map.get(bp.$container-max-widths, $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
assets/scss/basic/_dialog.scss
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
.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));
|
||||||
|
}
|
||||||
93
assets/scss/basic/_dropdown.scss
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The base geometry of .dropdown-button/.dropdown-toggle lives at the top of `_buttons.scss`, which
|
||||||
|
// is loaded before this partial, so that .button-sm/.button-lg/.button-* win the specificity tie
|
||||||
|
// against it (CSS resolves a tie by order of appearance in the file, not by the order of the
|
||||||
|
// classes in the 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;
|
||||||
|
}
|
||||||
|
// Anchors the menu's right edge to the button's right edge, so it opens inwards.
|
||||||
|
.dropdown-menu-end {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options and headers stay on one line, so the menu grows to fit the longest one.
|
||||||
|
.dropdown-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
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;
|
||||||
|
white-space: nowrap;
|
||||||
|
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);
|
||||||
|
}
|
||||||
110
assets/scss/basic/_form.scss
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
.form-required {
|
||||||
|
color: var(--val-color--danger);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field > select.form-select,
|
||||||
|
.form-field > input.form-control,
|
||||||
|
.form-field > input.form-range,
|
||||||
|
.form-field > textarea.form-control {
|
||||||
|
display: block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
background: var(--val-color--bg);
|
||||||
|
}
|
||||||
|
fieldset > legend {
|
||||||
|
background-color: var(--val-color--bg);
|
||||||
|
border: 2px groove threedface;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 1.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.form-check-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.form-check-reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.form-check-input {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.form-check-label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.form-switch .form-check-input {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 2em;
|
||||||
|
height: 1em;
|
||||||
|
background-color: var(--val-color--switch-off);
|
||||||
|
border-radius: 1em;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color .15s ease-in-out;
|
||||||
|
}
|
||||||
|
.form-switch .form-check-input::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: transform .15s ease-in-out;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, .25);
|
||||||
|
}
|
||||||
|
.form-switch .form-check-input:checked {
|
||||||
|
background-color: var(--val-color--primary);
|
||||||
|
}
|
||||||
|
.form-switch .form-check-input:checked::after {
|
||||||
|
transform: translateX(1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled,
|
||||||
|
input:disabled + label {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
input:disabled + label {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
background-color: #fff;
|
||||||
|
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
15
assets/scss/basic/_image.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
.image {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.image-fluid {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.image-thumbnail {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background-color: var(--val-color--bg);
|
||||||
|
border: 1px solid var(--val-color--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
24
assets/scss/basic/_messages.scss
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
.message {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-info {
|
||||||
|
color: color-mix(in srgb, var(--val-color--info) 40%, black);
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--info) 15%, var(--val-color--bg));
|
||||||
|
border-color: color-mix(in srgb, var(--val-color--info) 40%, var(--val-color--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-warning {
|
||||||
|
color: color-mix(in srgb, var(--val-color--warning) 40%, black);
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--warning) 15%, var(--val-color--bg));
|
||||||
|
border-color: color-mix(in srgb, var(--val-color--warning) 40%, var(--val-color--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-error {
|
||||||
|
color: color-mix(in srgb, var(--val-color--danger) 40%, black);
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--danger) 15%, var(--val-color--bg));
|
||||||
|
border-color: color-mix(in srgb, var(--val-color--danger) 40%, var(--val-color--bg));
|
||||||
|
}
|
||||||
61
assets/scss/basic/_nav.scss
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: var(--val-color--text);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.nav-link.disabled {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
// The toggle of a dropdown inside a `Nav` looks like the other links: it drops the border that a
|
||||||
|
// standalone `Dropdown` toggle has (see `_buttons.scss`).
|
||||||
|
.nav-link.dropdown-toggle {
|
||||||
|
width: 100%;
|
||||||
|
text-align: start;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
118
assets/scss/basic/_navbar.scss
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
@use "breakpoints" as bp;
|
||||||
|
|
||||||
|
.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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushes the toggle (`BrandLeft`) or the brand (`BrandRight`) to the far end of the bar.
|
||||||
|
.brand ~ .navbar-toggle,
|
||||||
|
.navbar-toggle ~ .brand {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
// A bar without a toggle button never stacks its `.nav` items in a column, so they wrap onto
|
||||||
|
// another line when they don't fit instead of overflowing the bar.
|
||||||
|
.navbar-content .nav {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-text {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navbar position, from the `navbar::Position` classes. `.navbar` has no background of its own, so
|
||||||
|
// a bar taken out of the flow (fixed) or pinned while scrolling (sticky) gets one, or the content
|
||||||
|
// passing underneath would show through.
|
||||||
|
.navbar.fixed-top,
|
||||||
|
.navbar.fixed-bottom,
|
||||||
|
.navbar.sticky-top,
|
||||||
|
.navbar.sticky-bottom {
|
||||||
|
background-color: var(--val-color--bg);
|
||||||
|
}
|
||||||
|
.navbar.fixed-top,
|
||||||
|
.navbar.fixed-bottom {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1030;
|
||||||
|
}
|
||||||
|
.navbar.fixed-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.navbar.fixed-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.navbar.sticky-top,
|
||||||
|
.navbar.sticky-bottom {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1020;
|
||||||
|
}
|
||||||
|
.navbar.sticky-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.navbar.sticky-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A navbar with a toggle button collapses below the breakpoint set by its `navbar-expand-{name}`
|
||||||
|
// class, added by the `Navbar` component. `navbar-expand` alone (a breakpoint with no minimum
|
||||||
|
// width) never collapses and needs no rules, since the base rules above are the expanded layout.
|
||||||
|
//
|
||||||
|
// While collapsed, the `.nav` items are stacked in a column, so a dropdown menu inside it is laid
|
||||||
|
// out in the flow (`position: static`) to push down the following items instead of covering them.
|
||||||
|
// A navbar without a toggle button has nothing to collapse: its `.nav` items stay in a row.
|
||||||
|
@each $name, $width in bp.$breakpoints {
|
||||||
|
@media (width < $width) {
|
||||||
|
.navbar-expand-#{$name} {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.navbar-expand-#{$name}:has(.navbar-toggle) {
|
||||||
|
.navbar-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.navbar-content {
|
||||||
|
order: 1;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.navbar-content.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.nav .dropdown-menu {
|
||||||
|
position: static;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
assets/scss/basic/_pager.scss
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
.pager {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
.pager-align-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.pager-align-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.pager-align-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.pager-summary {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-inline-end: auto;
|
||||||
|
}
|
||||||
|
.pager-align-start .pager-summary {
|
||||||
|
order: 1;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
.pager-align-center .pager-summary {
|
||||||
|
order: 1;
|
||||||
|
flex-basis: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.pagination a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: var(--val-color--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.pagination a:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
|
||||||
|
}
|
||||||
|
.pagination .active a {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--val-color--primary);
|
||||||
|
}
|
||||||
|
.pagination .disabled a {
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-ellipsis {
|
||||||
|
padding: 0.375rem 0.25rem;
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// .page-link-icon shows the navigation text of the previous/next buttons. The full accessible text
|
||||||
|
// goes separately, in the link's aria-label. So replacing the visible text with other content does
|
||||||
|
// not affect accessibility, e.g. to use "‹"/"›" you can apply:
|
||||||
|
//
|
||||||
|
// .page-link-icon {
|
||||||
|
// display: inline-block;
|
||||||
|
// font-size: 0;
|
||||||
|
// }
|
||||||
|
// .page-previous .page-link-icon::before {
|
||||||
|
// content: "‹";
|
||||||
|
// font-size: 1rem;
|
||||||
|
// display: inline-block;
|
||||||
|
// transform: translateY(-0.15em) scale(1.5);
|
||||||
|
// }
|
||||||
|
// .page-next .page-link-icon::before {
|
||||||
|
// content: "›";
|
||||||
|
// font-size: 1rem;
|
||||||
|
// display: inline-block;
|
||||||
|
// transform: translateY(-0.15em) scale(1.5);
|
||||||
|
// }
|
||||||
|
|
||||||
|
.pager-jump {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.pager-jump:focus-within {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--val-color--primary) 25%, transparent);
|
||||||
|
}
|
||||||
|
.pager-jump-input > input.form-control:focus,
|
||||||
|
.pager-jump-button:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.pager-jump-input > input.form-control {
|
||||||
|
width: calc(var(--pager-jump-width, 1ch) + 2.5rem);
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
line-height: var(--val-lh--base);
|
||||||
|
border: 0;
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
background-color: var(--val-color--bg--muted);
|
||||||
|
}
|
||||||
|
.pager-jump-input > input.form-control {
|
||||||
|
text-align: center;
|
||||||
|
appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
.pager-jump-input > input.form-control::-webkit-outer-spin-button,
|
||||||
|
.pager-jump-input > input.form-control::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pager-jump-button {
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border: 0;
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
}
|
||||||
|
.pager-jump-button:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
||||||
|
}
|
||||||
4
assets/scss/basic/_regions.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.region-footer {
|
||||||
|
padding: .75rem 0 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
27
assets/scss/basic/_root.scss
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
:root {
|
||||||
|
// Font families
|
||||||
|
--val-font-sans: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif;
|
||||||
|
--val-font-serif: Georgia,"Times New Roman",serif;
|
||||||
|
--val-font-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||||
|
--val-font-family: var(--val-font-sans);
|
||||||
|
// Font size
|
||||||
|
--val-fs--base: 1rem;
|
||||||
|
// Font weight
|
||||||
|
--val-fw--base: 400;
|
||||||
|
// Line height
|
||||||
|
--val-lh--base: 1.5;
|
||||||
|
// Core Colors
|
||||||
|
--val-color--primary: #0d6efd;
|
||||||
|
--val-color--secondary: #6c757d;
|
||||||
|
--val-color--success: #198754;
|
||||||
|
--val-color--info: #0dcaf0;
|
||||||
|
--val-color--warning: #ffc107;
|
||||||
|
--val-color--danger: #dc3545;
|
||||||
|
// Other Colors
|
||||||
|
--val-color--bg: #fafafa;
|
||||||
|
--val-color--bg--muted: #dee2e6;
|
||||||
|
--val-color--text: #212529;
|
||||||
|
--val-color--text--muted: color-mix(in srgb, var(--val-color--text) 50%, var(--val-color--bg));
|
||||||
|
--val-color--switch-off: #adb5bd;
|
||||||
|
--val-color--border: #212529;
|
||||||
|
}
|
||||||
51
assets/scss/basic/_table.scss
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
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;
|
||||||
|
color: var(--val-color--text--muted);
|
||||||
|
}
|
||||||
55
assets/scss/basic/_user.scss
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
.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-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;
|
||||||
|
}
|
||||||
12
assets/scss/basic/_utilities.scss
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
522
assets/scss/intro.scss
Normal file
|
|
@ -0,0 +1,522 @@
|
||||||
|
// Styles of the `Intro` component.
|
||||||
|
//
|
||||||
|
// Compiled by `build.rs` into `static/css/intro.min.css`, apart from `basic.scss`: the component
|
||||||
|
// loads them on its own, whatever the active theme is.
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--intro-bg-img: url('/pagetop/img/intro-header.jpg');
|
||||||
|
--intro-bg-img-set: image-set(url('/pagetop/img/intro-header.avif') type('image/avif'), url('/pagetop/img/intro-header.webp') type('image/webp'), var(--intro-bg-img) type('image/jpeg'));
|
||||||
|
--intro-bg-img-sm: url('/pagetop/img/intro-header-sm.jpg');
|
||||||
|
--intro-bg-img-sm-set: image-set(url('/pagetop/img/intro-header-sm.avif') type('image/avif'), url('/pagetop/img/intro-header-sm.webp') type('image/webp'), var(--intro-bg-img-sm) type('image/jpeg'));
|
||||||
|
--intro-bg-color: #7a430e;
|
||||||
|
--intro-bg-block-1: #ffb84b;
|
||||||
|
--intro-bg-block-2: #ffc66f;
|
||||||
|
--intro-bg-block-3: #ffd493;
|
||||||
|
--intro-bg-block-4: #ffe3b7;
|
||||||
|
--intro-bg-block-5: #fff1db;
|
||||||
|
--intro-bg-block-6: #ffffff;
|
||||||
|
--intro-color: #1a202c;
|
||||||
|
--intro-color-gray: #e8e3da;
|
||||||
|
--intro-color-link: #8b2500;
|
||||||
|
--intro-focus-outline: 2px solid var(--intro-color-link);
|
||||||
|
--intro-focus-outline-offset: 2px;
|
||||||
|
--intro-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-x: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media queries: 768px and 992px match the default `Md` and `Lg` breakpoints of the core.
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
position: relative;
|
||||||
|
color: var(--intro-color);
|
||||||
|
background-color: var(--intro-bg-color);
|
||||||
|
|
||||||
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
|
margin-left: -50vw;
|
||||||
|
margin-right: -50vw;
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro section {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intro Header
|
||||||
|
|
||||||
|
.intro-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
background-image: var(--intro-bg-img-sm);
|
||||||
|
background-image: var(--intro-bg-img-sm-set);
|
||||||
|
background-position: top center;
|
||||||
|
background-position-y: -1rem;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.intro-header-body {
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.intro-header-title {
|
||||||
|
margin: 0 0 0 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #dceefb;
|
||||||
|
padding: clamp(0rem, -5.4892rem + 23.4206vw, 9.5rem) 1rem 1rem;
|
||||||
|
font-size: clamp(1.5rem, 0.7231rem + 3.3149vw, 3.375rem);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 110%;
|
||||||
|
text-shadow: 0 0.125rem 0.1875rem rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.intro-header-title > span {
|
||||||
|
background: linear-gradient(180deg, #ddff95 30%, #ffb84b 100%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-size: clamp(2.25rem, 1.3177rem + 3.9779vw, 6.5rem);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 135%;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
.intro-header-img {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-header-mascot {
|
||||||
|
margin: 2rem;
|
||||||
|
flex-shrink: 1;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
@media (width >= 992px) {
|
||||||
|
.intro-header {
|
||||||
|
background-image: var(--intro-bg-img);
|
||||||
|
background-image: var(--intro-bg-img-set);
|
||||||
|
}
|
||||||
|
.intro-header-title {
|
||||||
|
padding: 0 2rem 2.6rem 2rem;
|
||||||
|
}
|
||||||
|
.intro-header-img {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.intro-header-mascot {
|
||||||
|
margin-right: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intro Content
|
||||||
|
|
||||||
|
.intro-content {
|
||||||
|
height: auto;
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-content-body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 80rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.intro-content-body:before,
|
||||||
|
.intro-content-body:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(130deg, rgba(13, 44, 91, 0) 0%, #ddff95 77.4%);
|
||||||
|
margin: 0 -1.2rem;
|
||||||
|
filter: blur(2.75rem);
|
||||||
|
opacity: 0.8;
|
||||||
|
inset: unset;
|
||||||
|
}
|
||||||
|
.intro-content-body:before {
|
||||||
|
top: -1rem;
|
||||||
|
}
|
||||||
|
.intro-content-body:after {
|
||||||
|
bottom: -1rem;
|
||||||
|
}
|
||||||
|
@media (width < 768px) {
|
||||||
|
.intro-content-body {
|
||||||
|
margin-top: -9.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (width >= 992px) {
|
||||||
|
.intro-content {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.intro-content-body {
|
||||||
|
margin-top: -5.7rem;
|
||||||
|
}
|
||||||
|
.intro-content-body:before,
|
||||||
|
.intro-content-body:after {
|
||||||
|
inset: 11.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.intro-button-link {
|
||||||
|
background: #7f1d1d;
|
||||||
|
background-image: linear-gradient(to bottom, rgba(255,0,0,0.8), rgba(255,255,255,0));
|
||||||
|
background-position: top left, center;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-decoration: none !important;
|
||||||
|
transition: transform 0.3s ease-in-out !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 28.875rem;
|
||||||
|
min-height: 7.6875rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.intro-button-link:focus-visible {
|
||||||
|
outline: var(--intro-focus-outline);
|
||||||
|
outline-offset: var(--intro-focus-outline-offset);
|
||||||
|
}
|
||||||
|
.intro-button-link::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -13.125rem;
|
||||||
|
left: -10rem;
|
||||||
|
height: 26.25rem;
|
||||||
|
width: 26.25rem;
|
||||||
|
background: linear-gradient(135deg, #ec7bae 50.41%, #9600b8 70.41%);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.intro-button-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.65rem;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 130.023%;
|
||||||
|
letter-spacing: 0.0075rem;
|
||||||
|
}
|
||||||
|
.intro-button-text strong {
|
||||||
|
font-size: 2.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 130.023%;
|
||||||
|
letter-spacing: 0.013125rem;
|
||||||
|
}
|
||||||
|
.intro-button-link span {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.intro-button-link span:nth-child(1) {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to right, rgba(0, 0, 0, 0), #f6e58d);
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
animation: span1 8s linear infinite;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
@keyframes span1 {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.intro-button-link span:nth-child(2) {
|
||||||
|
width: 8px;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), #f6e58d);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
transform: translateY(100%);
|
||||||
|
animation: span2 4s linear infinite;
|
||||||
|
animation-delay: 5s;
|
||||||
|
}
|
||||||
|
@keyframes span2 {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.intro-button-link span:nth-child(3) {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to left, rgba(0, 0, 0, 0), #f6e58d);
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
transform: translateX(100%);
|
||||||
|
animation: span3 8s linear infinite;
|
||||||
|
animation-delay: 7s;
|
||||||
|
}
|
||||||
|
@keyframes span3 {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.intro-button-link:hover span {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
@media (width < 768px) {
|
||||||
|
.intro-header {
|
||||||
|
padding-bottom: 9rem;;
|
||||||
|
}
|
||||||
|
.intro-button-link {
|
||||||
|
height: 6.25rem;
|
||||||
|
min-width: auto;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.intro-button-text {
|
||||||
|
display: inline;
|
||||||
|
padding-top: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (width >= 768px) {
|
||||||
|
.intro-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 29.375rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.intro-button-link:hover {
|
||||||
|
transition: all .5s;
|
||||||
|
transform: rotate(-3deg) scale(1.125);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.intro-text-body {
|
||||||
|
padding: 2.5rem 1.063rem 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-text-lead {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
@media (width >= 768px) {
|
||||||
|
.intro-button + .intro-text-body {
|
||||||
|
padding-top: 7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (width >= 992px) {
|
||||||
|
.intro-header {
|
||||||
|
padding-bottom: 9rem;;
|
||||||
|
}
|
||||||
|
.intro-text,
|
||||||
|
.intro-text-body {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
.intro-text {
|
||||||
|
box-shadow: var(--intro-shadow);
|
||||||
|
max-width: min(60rem, 100vw - 4rem);
|
||||||
|
margin: 0 auto 6rem;
|
||||||
|
}
|
||||||
|
.intro-text-narrow {
|
||||||
|
max-width: min(50rem, 100vw - 4rem);
|
||||||
|
}
|
||||||
|
.intro-text-wide {
|
||||||
|
max-width: min(67rem, 100vw - 4rem);
|
||||||
|
}
|
||||||
|
.intro-text-full {
|
||||||
|
max-width: min(74rem, 100vw - 4rem);
|
||||||
|
}
|
||||||
|
.intro-text-body {
|
||||||
|
padding-left: 4.5rem;
|
||||||
|
padding-right: 4.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text-body .block {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.intro-text-body .block-title {
|
||||||
|
margin: 1em 0 .8em;
|
||||||
|
}
|
||||||
|
.intro-text-body .block-title span {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 30px 14px;
|
||||||
|
margin: 30px 20px 0;
|
||||||
|
background: white;
|
||||||
|
border: 5px solid;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 0 0 5px white, inset 0 0 0 5px white;
|
||||||
|
border-color: orangered;
|
||||||
|
transform: rotate(-3deg) translateY(-25%);
|
||||||
|
}
|
||||||
|
.intro-text-body .block-title:before {
|
||||||
|
content: "";
|
||||||
|
height: 5px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
left: -15%;
|
||||||
|
width: 130%;
|
||||||
|
z-index: -5;
|
||||||
|
background: orangered;
|
||||||
|
box-shadow: 0 0 0 5px white, 0 -10px 0 5px white;
|
||||||
|
transform: rotate(2deg) translateY(-50%);
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
.intro-text-body .block-title:after {
|
||||||
|
content: "";
|
||||||
|
height: 120%;
|
||||||
|
position: absolute;
|
||||||
|
top: 42px;
|
||||||
|
left: -25%;
|
||||||
|
width: 150%;
|
||||||
|
z-index: -10;
|
||||||
|
background: var(--intro-bg-block-1);
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+1) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-1);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+2) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-2);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+3) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-3);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+4) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-4);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+5) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-5);
|
||||||
|
}
|
||||||
|
.intro-text-body .block:nth-of-type(6n+6) .block-title:after {
|
||||||
|
background: var(--intro-bg-block-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#intro-badges {
|
||||||
|
min-height: 3.2rem;
|
||||||
|
text-align: center;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#intro-badges img {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
#intro-release:not([src]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intro Footer
|
||||||
|
|
||||||
|
.intro-footer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: black;
|
||||||
|
color: var(--intro-color-gray);
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-footer-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 10.625rem;
|
||||||
|
max-width: 80rem;
|
||||||
|
}
|
||||||
|
.intro-footer-body a:visited {
|
||||||
|
color: var(--intro-color-gray);
|
||||||
|
}
|
||||||
|
// The footer has its own dark surface, so its links don't inherit the theme's link colors.
|
||||||
|
.intro-footer-links a {
|
||||||
|
color: currentColor;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: font-size 0.2s, text-decoration-color 0.2s;
|
||||||
|
}
|
||||||
|
.intro-footer-links a:focus-visible {
|
||||||
|
outline: var(--intro-focus-outline);
|
||||||
|
outline-offset: var(--intro-focus-outline-offset);
|
||||||
|
}
|
||||||
|
.intro-footer-links a:hover,
|
||||||
|
.intro-footer-links a:hover:visited {
|
||||||
|
text-decoration-color: var(--intro-color-link);
|
||||||
|
}
|
||||||
|
.intro-footer-logo,
|
||||||
|
.intro-footer-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-footer-logo {
|
||||||
|
max-height: 12.625rem;
|
||||||
|
}
|
||||||
|
.intro-footer-logo svg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.intro-footer-links {
|
||||||
|
gap: 1.875rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
@media (width < 768px) {
|
||||||
|
.intro-footer-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (width < 992px) {
|
||||||
|
.intro-footer-body {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
build.rs
|
|
@ -1,6 +1,34 @@
|
||||||
use pagetop_build::StaticFilesBundle;
|
use pagetop_build::{StaticFilesBundle, compile_scss, copy_dir, copy_file, minify_js};
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
|
// Regenera `static/` desde cero sólo si hay cambios en `assets/`.
|
||||||
|
println!("cargo:rerun-if-changed=assets");
|
||||||
|
let _ = std::fs::remove_dir_all("static");
|
||||||
|
|
||||||
|
// Copia sin transformar. `assets/scss/` queda fuera porque sólo contiene fuentes SCSS.
|
||||||
|
copy_file("assets/banner.png", "static/banner.png")?;
|
||||||
|
copy_file("assets/favicon.ico", "static/favicon.ico")?;
|
||||||
|
copy_dir("assets/img", "static/img")?;
|
||||||
|
copy_dir("assets/js", "static/js")?;
|
||||||
|
|
||||||
|
// CSS: compila los estilos del tema Basic.
|
||||||
|
compile_scss("assets/scss/basic.scss", "static/css/basic.min.css")?;
|
||||||
|
|
||||||
|
// CSS: compila los estilos del componente `Intro`, independientes del tema.
|
||||||
|
compile_scss("assets/scss/intro.scss", "static/css/intro.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)
|
StaticFilesBundle::from_dir("./static", None)
|
||||||
.with_name("assets")
|
.with_name("assets")
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::new().run()?.await
|
Application::new().await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,31 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use pagetop_bootsier::theme::*;
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
include_locales!(LOC from "examples/locale");
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct FormControls;
|
struct FormControls;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for FormControls {
|
impl Extension for FormControls {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(form_controls))
|
router.route("/", web::get(form_controls).post(form_controls))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::custom()
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_title(Lc::t("title", &LOC))
|
||||||
.with_title(L10n::t("title", &LOC))
|
.with_slogan(Lc::t("slogan", &LOC))
|
||||||
.with_slogan(L10n::t("slogan", &LOC))
|
|
||||||
.with_button(None::<(L10n, FnPathByContext)>)
|
|
||||||
// Bloque 1: casillas, interruptores y botones de opción.
|
// Bloque 1: casillas, interruptores y botones de opción.
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("block_selections", &LOC))
|
.with_title(Lc::t("block_selections", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
Form::new()
|
Form::new()
|
||||||
.with_id("form-selections")
|
.with_id("form-selections")
|
||||||
|
|
@ -36,69 +34,69 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
// Casillas e interruptores (form::Checkbox).
|
// Casillas e interruptores (form::Checkbox).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_checkbox", &LOC))
|
.with_legend(Lc::t("fieldset_checkbox", &LOC))
|
||||||
.with_description(L10n::t("desc_checkbox", &LOC))
|
.with_description(Lc::t("desc_checkbox", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Checkbox::new()
|
form::Checkbox::new()
|
||||||
.with_name("accept_terms")
|
.with_name("accept_terms")
|
||||||
.with_label(L10n::t("label_terms", &LOC))
|
.with_label(Lc::t("label_terms", &LOC))
|
||||||
.with_required(true),
|
.with_required(true),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Checkbox::new()
|
form::Checkbox::new()
|
||||||
.with_name("accept_marketing")
|
.with_name("accept_marketing")
|
||||||
.with_label(L10n::t("label_marketing", &LOC))
|
.with_label(Lc::t("label_marketing", &LOC))
|
||||||
.with_checked(true)
|
.with_checked(true)
|
||||||
.with_inline(true),
|
.with_inline(true),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Checkbox::new()
|
form::Checkbox::new()
|
||||||
.with_name("newsletter")
|
.with_name("newsletter")
|
||||||
.with_label(L10n::t("label_newsletter", &LOC))
|
.with_label(Lc::t("label_newsletter", &LOC))
|
||||||
.with_inline(true),
|
.with_inline(true),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Checkbox::switch()
|
form::Checkbox::switch()
|
||||||
.with_name("notifications")
|
.with_name("notifications")
|
||||||
.with_label(L10n::t("label_notifications", &LOC))
|
.with_label(Lc::t("label_notifications", &LOC))
|
||||||
.with_checked(true)
|
.with_checked(true)
|
||||||
.with_reverse(true),
|
.with_reverse(true),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Checkbox::switch()
|
form::Checkbox::switch()
|
||||||
.with_name("dark_mode")
|
.with_name("dark_mode")
|
||||||
.with_label(L10n::t("label_dark_mode", &LOC))
|
.with_label(Lc::t("label_dark_mode", &LOC))
|
||||||
.with_disabled(true),
|
.with_disabled(true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Grupo de casillas de verificación (form::check::Field).
|
// Grupo de casillas de verificación (form::check::Field).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_checkgroup", &LOC))
|
.with_legend(Lc::t("fieldset_checkgroup", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::check::Field::new()
|
form::check::Field::new()
|
||||||
.with_name("interests")
|
.with_name("interests")
|
||||||
.with_label(L10n::t("label_interests", &LOC))
|
.with_label(Lc::t("label_interests", &LOC))
|
||||||
.with_help_text(L10n::t("help_interests", &LOC))
|
.with_help_text(Lc::t("help_interests", &LOC))
|
||||||
.with_item(
|
.with_item(
|
||||||
form::check::Item::new(
|
form::check::Item::new(
|
||||||
"rust",
|
"rust",
|
||||||
L10n::t("check_rust", &LOC),
|
Lc::t("check_rust", &LOC),
|
||||||
)
|
)
|
||||||
.with_checked(true),
|
.with_checked(true),
|
||||||
)
|
)
|
||||||
.with_item(form::check::Item::new(
|
.with_item(form::check::Item::new(
|
||||||
"web",
|
"web",
|
||||||
L10n::t("check_web", &LOC),
|
Lc::t("check_web", &LOC),
|
||||||
))
|
))
|
||||||
.with_item(form::check::Item::new(
|
.with_item(form::check::Item::new(
|
||||||
"ai",
|
"ai",
|
||||||
L10n::t("check_ai", &LOC),
|
Lc::t("check_ai", &LOC),
|
||||||
))
|
))
|
||||||
.with_item(
|
.with_item(
|
||||||
form::check::Item::new(
|
form::check::Item::new(
|
||||||
"games",
|
"games",
|
||||||
L10n::t("check_games", &LOC),
|
Lc::t("check_games", &LOC),
|
||||||
)
|
)
|
||||||
.with_disabled(true),
|
.with_disabled(true),
|
||||||
),
|
),
|
||||||
|
|
@ -107,60 +105,62 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
// Botones de opción (form::radio::Field).
|
// Botones de opción (form::radio::Field).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_radio", &LOC))
|
.with_legend(Lc::t("fieldset_radio", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::radio::Field::new()
|
form::radio::Field::new()
|
||||||
.with_name("frequency")
|
.with_name("frequency")
|
||||||
.with_label(L10n::t("label_frequency", &LOC))
|
.with_label(Lc::t("label_frequency", &LOC))
|
||||||
.with_item(form::radio::Item::new(
|
.with_item(form::radio::Item::new(
|
||||||
"daily",
|
"daily",
|
||||||
L10n::t("radio_daily", &LOC),
|
Lc::t("radio_daily", &LOC),
|
||||||
))
|
))
|
||||||
.with_item(
|
.with_item(
|
||||||
form::radio::Item::new(
|
form::radio::Item::new(
|
||||||
"weekly",
|
"weekly",
|
||||||
L10n::t("radio_weekly", &LOC),
|
Lc::t("radio_weekly", &LOC),
|
||||||
)
|
)
|
||||||
.with_checked(true),
|
.with_checked(true),
|
||||||
)
|
)
|
||||||
.with_item(form::radio::Item::new(
|
.with_item(form::radio::Item::new(
|
||||||
"monthly",
|
"monthly",
|
||||||
L10n::t("radio_monthly", &LOC),
|
Lc::t("radio_monthly", &LOC),
|
||||||
))
|
))
|
||||||
.with_item(
|
.with_item(
|
||||||
form::radio::Item::new(
|
form::radio::Item::new(
|
||||||
"never",
|
"never",
|
||||||
L10n::t("radio_never", &LOC),
|
Lc::t("radio_never", &LOC),
|
||||||
)
|
)
|
||||||
.with_disabled(true),
|
.with_disabled(true),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Campo oculto (form::Hidden).
|
// Campo oculto (form::Hidden).
|
||||||
|
.with_child(form::Hidden::field("origin", "form-selections"))
|
||||||
|
// Botonera de acciones.
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Hidden::new()
|
Flex::new()
|
||||||
.with_name("origin")
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_value("form-selections"),
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
)
|
.with_child(
|
||||||
// Botones de acción.
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
.with_child(
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
Button::submit(L10n::t("btn_submit", &LOC))
|
)
|
||||||
.with_color(ButtonColor::Background(Color::Primary)),
|
.with_child(
|
||||||
)
|
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||||
.with_child(
|
button::Style::Outline(Intent::Neutral),
|
||||||
Button::reset(L10n::t("btn_reset", &LOC))
|
),
|
||||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
)
|
||||||
)
|
.with_child(
|
||||||
.with_child(
|
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
.with_style(button::Style::Link),
|
||||||
.with_color(ButtonColor::Link),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Bloque 2: campos de texto, multilínea y rango.
|
// Bloque 2: campos de texto, multilínea y rango.
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("block_text", &LOC))
|
.with_title(Lc::t("block_text", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
Form::new()
|
Form::new()
|
||||||
.with_id("form-text")
|
.with_id("form-text")
|
||||||
|
|
@ -169,22 +169,21 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
// Campos de texto (form::input::Field).
|
// Campos de texto (form::input::Field).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_text", &LOC))
|
.with_legend(Lc::t("fieldset_text", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::text()
|
form::input::Field::text()
|
||||||
.with_name("name")
|
.with_name("name")
|
||||||
.with_label(L10n::t("label_name", &LOC))
|
.with_label(Lc::t("label_name", &LOC))
|
||||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
.with_placeholder(Lc::t("placeholder_name", &LOC))
|
||||||
|
.with_help_text(Lc::t("help_name", &LOC))
|
||||||
.with_required(true),
|
.with_required(true),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::email()
|
form::input::Field::email()
|
||||||
.with_name("email")
|
.with_name("email")
|
||||||
.with_label(L10n::t("label_email", &LOC))
|
.with_label(Lc::t("label_email", &LOC))
|
||||||
.with_placeholder(L10n::t(
|
.with_placeholder(Lc::t("placeholder_email", &LOC))
|
||||||
"placeholder_email",
|
.with_help_text(Lc::t("help_email", &LOC))
|
||||||
&LOC,
|
|
||||||
))
|
|
||||||
.with_autocomplete(
|
.with_autocomplete(
|
||||||
Some(form::Autocomplete::email()),
|
Some(form::Autocomplete::email()),
|
||||||
)
|
)
|
||||||
|
|
@ -193,7 +192,7 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::password()
|
form::input::Field::password()
|
||||||
.with_name("password")
|
.with_name("password")
|
||||||
.with_label(L10n::t("label_password", &LOC))
|
.with_label(Lc::t("label_password", &LOC))
|
||||||
.with_autocomplete(Some(
|
.with_autocomplete(Some(
|
||||||
form::Autocomplete::new_password(),
|
form::Autocomplete::new_password(),
|
||||||
))
|
))
|
||||||
|
|
@ -202,23 +201,20 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::telephone()
|
form::input::Field::telephone()
|
||||||
.with_name("phone")
|
.with_name("phone")
|
||||||
.with_label(L10n::t("label_phone", &LOC))
|
.with_label(Lc::t("label_phone", &LOC))
|
||||||
.with_placeholder(L10n::t(
|
.with_placeholder(Lc::t("placeholder_phone", &LOC)),
|
||||||
"placeholder_phone",
|
|
||||||
&LOC,
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::url()
|
form::input::Field::url()
|
||||||
.with_name("website")
|
.with_name("website")
|
||||||
.with_label(L10n::t("label_url", &LOC))
|
.with_label(Lc::t("label_url", &LOC))
|
||||||
.with_placeholder(L10n::t("placeholder_url", &LOC)),
|
.with_placeholder(Lc::t("placeholder_url", &LOC)),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
form::input::Field::search()
|
form::input::Field::search()
|
||||||
.with_name("search")
|
.with_name("search")
|
||||||
.with_label(L10n::t("label_search", &LOC))
|
.with_label(Lc::t("label_search", &LOC))
|
||||||
.with_placeholder(L10n::t(
|
.with_placeholder(Lc::t(
|
||||||
"placeholder_search",
|
"placeholder_search",
|
||||||
&LOC,
|
&LOC,
|
||||||
)),
|
)),
|
||||||
|
|
@ -227,230 +223,233 @@ async fn form_controls(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
// Área de texto (form::Textarea).
|
// Área de texto (form::Textarea).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_textarea", &LOC))
|
.with_legend(Lc::t("fieldset_textarea", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Textarea::new()
|
form::Textarea::new()
|
||||||
.with_name("comment")
|
.with_name("comment")
|
||||||
.with_label(L10n::t("label_comment", &LOC))
|
.with_label(Lc::t("label_comment", &LOC))
|
||||||
.with_placeholder(L10n::t(
|
.with_placeholder(Lc::t(
|
||||||
"placeholder_comment",
|
"placeholder_comment",
|
||||||
&LOC,
|
&LOC,
|
||||||
))
|
))
|
||||||
.with_rows(Some(4))
|
.with_rows(Some(4))
|
||||||
.with_help_text(L10n::t("help_comment", &LOC)),
|
.with_help_text(Lc::t("help_comment", &LOC)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Control deslizante (form::Range).
|
// Control deslizante (form::Range).
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Fieldset::new()
|
form::Fieldset::new()
|
||||||
.with_legend(L10n::t("fieldset_range", &LOC))
|
.with_legend(Lc::t("fieldset_range", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Range::new()
|
form::Range::new()
|
||||||
.with_name("rating")
|
.with_name("rating")
|
||||||
.with_label(L10n::t("label_rating", &LOC))
|
.with_label(Lc::t("label_rating", &LOC))
|
||||||
.with_min(Some(1.0))
|
.with_min(Some(1.0))
|
||||||
.with_max(Some(10.0))
|
.with_max(Some(10.0))
|
||||||
.with_step(Some(1.0))
|
.with_step(Some(1.0))
|
||||||
.with_value(Some(5.0))
|
.with_value(Some(5.0))
|
||||||
.with_help_text(L10n::t("help_rating", &LOC)),
|
.with_help_text(Lc::t("help_rating", &LOC)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Campo oculto (form::Hidden).
|
// Campo oculto (form::Hidden).
|
||||||
|
.with_child(form::Hidden::field("origin", "form-text"))
|
||||||
|
// Botonera de acciones.
|
||||||
.with_child(
|
.with_child(
|
||||||
form::Hidden::new()
|
Flex::new()
|
||||||
.with_name("origin")
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
.with_value("form-text"),
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
)
|
.with_child(
|
||||||
// Botones de acción.
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
.with_child(
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
Button::submit(L10n::t("btn_submit", &LOC))
|
)
|
||||||
.with_color(ButtonColor::Background(Color::Primary)),
|
.with_child(
|
||||||
)
|
Button::reset(Lc::t("btn_reset", &LOC)).with_style(
|
||||||
.with_child(
|
button::Style::Outline(Intent::Neutral),
|
||||||
Button::reset(L10n::t("btn_reset", &LOC))
|
),
|
||||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
)
|
||||||
)
|
.with_child(
|
||||||
.with_child(
|
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
.with_style(button::Style::Link),
|
||||||
.with_color(ButtonColor::Link),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// Bloque 3: listas de selección y etiquetas flotantes.
|
// Bloque 3: listas de selección y etiquetas flotantes.
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("block_lists", &LOC))
|
.with_title(
|
||||||
|
if global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") {
|
||||||
|
Lc::t("block_lists_floating", &LOC)
|
||||||
|
} else {
|
||||||
|
Lc::t("block_lists", &LOC)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_child(form_lists()),
|
||||||
|
)
|
||||||
|
// Bloque 4: diálogo modal.
|
||||||
|
.with_child(
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("block_dialog", &LOC))
|
||||||
.with_child(
|
.with_child(
|
||||||
Form::new()
|
Dialog::new()
|
||||||
.with_id("form-lists")
|
.with_id("delete-confirm")
|
||||||
.with_action("/")
|
.with_title(Lc::t("dialog_delete_title", &LOC))
|
||||||
.with_method(form::Method::Post)
|
.with_child(Html::with(|cx| {
|
||||||
// Listas de selección (form::select::Field).
|
html! {
|
||||||
.with_child(
|
p { (Lc::t("dialog_delete_body", &LOC).using(cx)) }
|
||||||
form::Fieldset::new()
|
}
|
||||||
.with_legend(L10n::t("fieldset_select", &LOC))
|
}))
|
||||||
.with_child(
|
.with_footer(
|
||||||
form::select::Field::new()
|
Button::plain(Lc::t("btn_ok", &LOC))
|
||||||
.with_name("language")
|
.with_prop(PropsOp::set("data-dialog-dismiss", "modal"))
|
||||||
.with_label(L10n::t("label_language", &LOC))
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
.with_item(
|
|
||||||
form::select::Item::new(
|
|
||||||
"",
|
|
||||||
L10n::t("select_choose", &LOC),
|
|
||||||
)
|
|
||||||
.with_selected(true),
|
|
||||||
)
|
|
||||||
.with_group(
|
|
||||||
form::select::Group::new(L10n::t(
|
|
||||||
"select_group_europe",
|
|
||||||
&LOC,
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"es",
|
|
||||||
L10n::t("select_spanish", &LOC),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"fr",
|
|
||||||
L10n::t("select_french", &LOC),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_group(
|
|
||||||
form::select::Group::new(L10n::t(
|
|
||||||
"select_group_americas",
|
|
||||||
&LOC,
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"en",
|
|
||||||
L10n::t("select_english", &LOC),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"pt",
|
|
||||||
L10n::t("select_portuguese", &LOC),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.with_item(
|
|
||||||
form::select::Item::new(
|
|
||||||
"xx",
|
|
||||||
L10n::t("select_disabled", &LOC),
|
|
||||||
)
|
|
||||||
.with_disabled(true),
|
|
||||||
)
|
|
||||||
.with_required(true),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
form::select::Field::new()
|
|
||||||
.with_name("technologies")
|
|
||||||
.with_label(L10n::t("label_technologies", &LOC))
|
|
||||||
.with_item(
|
|
||||||
form::select::Item::new(
|
|
||||||
"rust",
|
|
||||||
L10n::n("Rust"),
|
|
||||||
)
|
|
||||||
.with_selected(true),
|
|
||||||
)
|
|
||||||
.with_item(
|
|
||||||
form::select::Item::new(
|
|
||||||
"python",
|
|
||||||
L10n::n("Python"),
|
|
||||||
)
|
|
||||||
.with_selected(true),
|
|
||||||
)
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"javascript",
|
|
||||||
L10n::n("JavaScript"),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"go",
|
|
||||||
L10n::n("Go"),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"typescript",
|
|
||||||
L10n::n("TypeScript"),
|
|
||||||
))
|
|
||||||
.with_multiple(true)
|
|
||||||
.with_rows(Some(4))
|
|
||||||
.with_help_text(L10n::t("help_technologies", &LOC)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
// Etiquetas flotantes.
|
.with_footer(
|
||||||
.with_child(
|
Button::plain(Lc::t("btn_cancel", &LOC))
|
||||||
form::Fieldset::new()
|
.with_prop(PropsOp::set("data-dialog-dismiss", "modal"))
|
||||||
.with_legend(L10n::t("fieldset_floating", &LOC))
|
.with_style(button::Style::Outline(Intent::Neutral)),
|
||||||
.with_child(
|
|
||||||
form::input::Field::text()
|
|
||||||
.with_name("fl_name")
|
|
||||||
.with_label(L10n::t("label_name", &LOC))
|
|
||||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
|
||||||
.with_floating_label(true)
|
|
||||||
.with_required(true),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
form::Textarea::new()
|
|
||||||
.with_name("fl_comment")
|
|
||||||
.with_label(L10n::t("label_comment", &LOC))
|
|
||||||
.with_placeholder(L10n::t(
|
|
||||||
"placeholder_comment",
|
|
||||||
&LOC,
|
|
||||||
))
|
|
||||||
.with_floating_label(true),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
form::select::Field::new()
|
|
||||||
.with_name("fl_country")
|
|
||||||
.with_label(L10n::t("label_country", &LOC))
|
|
||||||
.with_item(
|
|
||||||
form::select::Item::new(
|
|
||||||
"",
|
|
||||||
L10n::t("select_choose", &LOC),
|
|
||||||
)
|
|
||||||
.with_selected(true),
|
|
||||||
)
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"de",
|
|
||||||
L10n::t("select_germany", &LOC),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"es",
|
|
||||||
L10n::t("select_spain", &LOC),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"fr",
|
|
||||||
L10n::t("select_france", &LOC),
|
|
||||||
))
|
|
||||||
.with_item(form::select::Item::new(
|
|
||||||
"pt",
|
|
||||||
L10n::t("select_portugal", &LOC),
|
|
||||||
))
|
|
||||||
.with_floating_label(true)
|
|
||||||
.with_required(true),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Campo oculto (form::Hidden).
|
|
||||||
.with_child(
|
|
||||||
form::Hidden::new()
|
|
||||||
.with_name("origin")
|
|
||||||
.with_value("form-lists"),
|
|
||||||
)
|
|
||||||
// Botones de acción.
|
|
||||||
.with_child(
|
|
||||||
Button::submit(L10n::t("btn_submit", &LOC))
|
|
||||||
.with_color(ButtonColor::Background(Color::Primary)),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
Button::reset(L10n::t("btn_reset", &LOC))
|
|
||||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
|
||||||
)
|
|
||||||
.with_child(
|
|
||||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
|
||||||
.with_color(ButtonColor::Link),
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::plain(Lc::t("btn_delete", &LOC))
|
||||||
|
.with_prop(PropsOp::set("data-dialog-toggle", "modal"))
|
||||||
|
.with_prop(PropsOp::set("data-dialog-target", "#delete-confirm"))
|
||||||
|
.with_style(button::Style::Solid(Intent::Severe)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn form_lists() -> Form {
|
||||||
|
let mut form = Form::new()
|
||||||
|
.with_id("form-lists")
|
||||||
|
.with_action("/")
|
||||||
|
.with_method(form::Method::Post)
|
||||||
|
// Listas de selección (form::select::Field).
|
||||||
|
.with_child(
|
||||||
|
form::Fieldset::new()
|
||||||
|
.with_legend(Lc::t("fieldset_select", &LOC))
|
||||||
|
.with_child(
|
||||||
|
form::select::Field::new()
|
||||||
|
.with_name("language")
|
||||||
|
.with_label(Lc::t("label_language", &LOC))
|
||||||
|
.with_item(
|
||||||
|
form::select::Item::new("", Lc::t("select_choose", &LOC))
|
||||||
|
.with_selected(true),
|
||||||
|
)
|
||||||
|
.with_group(
|
||||||
|
form::select::Group::new(Lc::t("select_group_europe", &LOC))
|
||||||
|
.with_item(form::select::Item::new(
|
||||||
|
"es",
|
||||||
|
Lc::t("select_spanish", &LOC),
|
||||||
|
))
|
||||||
|
.with_item(form::select::Item::new(
|
||||||
|
"fr",
|
||||||
|
Lc::t("select_french", &LOC),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_group(
|
||||||
|
form::select::Group::new(Lc::t("select_group_americas", &LOC))
|
||||||
|
.with_item(form::select::Item::new(
|
||||||
|
"en",
|
||||||
|
Lc::t("select_english", &LOC),
|
||||||
|
))
|
||||||
|
.with_item(form::select::Item::new(
|
||||||
|
"pt",
|
||||||
|
Lc::t("select_portuguese", &LOC),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_item(
|
||||||
|
form::select::Item::new("xx", Lc::t("select_disabled", &LOC))
|
||||||
|
.with_disabled(true),
|
||||||
|
)
|
||||||
|
.with_required(true),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
form::select::Field::new()
|
||||||
|
.with_name("technologies")
|
||||||
|
.with_label(Lc::t("label_technologies", &LOC))
|
||||||
|
.with_item(
|
||||||
|
form::select::Item::new("rust", Lc::n("Rust")).with_selected(true),
|
||||||
|
)
|
||||||
|
.with_item(
|
||||||
|
form::select::Item::new("python", Lc::n("Python")).with_selected(true),
|
||||||
|
)
|
||||||
|
.with_item(form::select::Item::new("javascript", Lc::n("JavaScript")))
|
||||||
|
.with_item(form::select::Item::new("go", Lc::n("Go")))
|
||||||
|
.with_item(form::select::Item::new("typescript", Lc::n("TypeScript")))
|
||||||
|
.with_multiple(true)
|
||||||
|
.with_rows(Some(4))
|
||||||
|
.with_help_text(Lc::t("help_technologies", &LOC)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Etiquetas flotantes: solo disponibles con el tema Bootsier.
|
||||||
|
if global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") {
|
||||||
|
form = form.with_child(
|
||||||
|
form::Fieldset::new()
|
||||||
|
.with_legend(Lc::t("fieldset_floating", &LOC))
|
||||||
|
.with_child(
|
||||||
|
form::input::Field::text()
|
||||||
|
.with_name("fl_name")
|
||||||
|
.with_label(Lc::t("label_name", &LOC))
|
||||||
|
.with_placeholder(Lc::t("placeholder_name", &LOC))
|
||||||
|
.with_floating_label(true)
|
||||||
|
.with_required(true),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
form::Textarea::new()
|
||||||
|
.with_name("fl_comment")
|
||||||
|
.with_label(Lc::t("label_comment", &LOC))
|
||||||
|
.with_placeholder(Lc::t("placeholder_comment", &LOC))
|
||||||
|
.with_floating_label(true),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
form::select::Field::new()
|
||||||
|
.with_name("fl_country")
|
||||||
|
.with_label(Lc::t("label_country", &LOC))
|
||||||
|
.with_item(
|
||||||
|
form::select::Item::new("", Lc::t("select_choose", &LOC))
|
||||||
|
.with_selected(true),
|
||||||
|
)
|
||||||
|
.with_item(form::select::Item::new("de", Lc::t("select_germany", &LOC)))
|
||||||
|
.with_item(form::select::Item::new("es", Lc::t("select_spain", &LOC)))
|
||||||
|
.with_item(form::select::Item::new("fr", Lc::t("select_france", &LOC)))
|
||||||
|
.with_item(form::select::Item::new(
|
||||||
|
"pt",
|
||||||
|
Lc::t("select_portugal", &LOC),
|
||||||
|
))
|
||||||
|
.with_floating_label(true)
|
||||||
|
.with_required(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
form
|
||||||
|
// Campo oculto (form::Hidden).
|
||||||
|
.with_child(form::Hidden::field("origin", "form-lists"))
|
||||||
|
// Botonera de acciones.
|
||||||
|
.with_child(
|
||||||
|
Flex::new()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
|
.with_child(
|
||||||
|
Button::submit(Lc::t("btn_submit", &LOC))
|
||||||
|
.with_style(button::Style::Solid(Intent::Primary)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::reset(Lc::t("btn_reset", &LOC))
|
||||||
|
.with_style(button::Style::Outline(Intent::Neutral)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::Style::Link),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&FormControls).run()?.await
|
Application::prepare(&FormControls).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloName;
|
struct HelloName;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloName {
|
impl Extension for HelloName {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/hello/{name}", web::get(hello_name))
|
router.route("/hello/{name}", web::get(hello_name))
|
||||||
|
|
@ -19,9 +20,10 @@ async fn hello_name(
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&HelloName).run()?.await
|
Application::prepare(&HelloName).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct HelloWorld;
|
struct HelloWorld;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for HelloWorld {
|
impl Extension for HelloWorld {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(hello_world))
|
router.route("/", web::get(hello_world))
|
||||||
|
|
@ -16,9 +17,10 @@ async fn hello_world(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&HelloWorld).run()?.await
|
Application::prepare(&HelloWorld).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct IntroColors;
|
struct IntroColors;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for IntroColors {
|
impl Extension for IntroColors {
|
||||||
fn configure_router(&self, router: Router) -> Router {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
router.route("/", web::get(intro_colors))
|
router.route("/", web::get(intro_colors))
|
||||||
|
|
@ -13,70 +14,69 @@ impl Extension for IntroColors {
|
||||||
async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn intro_colors(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_child(
|
.with_child(
|
||||||
Intro::default()
|
Intro::custom()
|
||||||
.with_opening(IntroOpening::Custom)
|
.with_title(Lc::n("PageTop"))
|
||||||
.with_title(L10n::n("PageTop"))
|
.with_slogan(Lc::t("colors_slogan", &LOC))
|
||||||
.with_slogan(L10n::t("colors_slogan", &LOC))
|
|
||||||
.with_button(None::<(L10n, FnPathByContext)>)
|
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "1"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_1", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_1", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "2"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "2"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_2", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_2", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "3"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "3"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_3", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_3", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "4"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "4"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_4", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_4", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "5"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "5"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_5", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_5", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "6"))
|
.with_title(Lc::t("colors_block", &LOC).with_arg("n", "6"))
|
||||||
.with_child(Html::with(|cx| {
|
.with_child(Html::with(|cx| {
|
||||||
html! {
|
html! {
|
||||||
p { (L10n::t("colors_val_6", &LOC).using(cx)) }
|
p { (Lc::t("colors_val_6", &LOC).using(cx)) }
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.render()
|
.render()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&IntroColors).run()?.await
|
Application::prepare(&IntroColors).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
270
examples/intro-datetime.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroDatetime;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroDatetime {
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_datetime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_datetime(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_child(
|
||||||
|
Intro::custom()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("datetime_slogan", &LOC))
|
||||||
|
.with_child(world_block())
|
||||||
|
.with_child(formats_block())
|
||||||
|
.with_child(relative_block())
|
||||||
|
.with_child(since_until_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zonas IANA de la demostración, con la clave de su etiqueta traducible.
|
||||||
|
const ZONES: [(&str, &str); 4] = [
|
||||||
|
("datetime_zone_utc", "UTC"),
|
||||||
|
("datetime_zone_madrid", "Europe/Madrid"),
|
||||||
|
("datetime_zone_mexico", "America/Mexico_City"),
|
||||||
|
("datetime_zone_tokyo", "Asia/Tokyo"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// El mismo instante (`Utc::now()`) mostrado en cada zona de `ZONES`: fecha y hora combinadas
|
||||||
|
// (`DateFormat::Medium` + `TimeFormat::Short`) y su equivalente ISO 8601, con el offset propio de
|
||||||
|
// cada zona.
|
||||||
|
fn world_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("datetime_block_world", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
let now = Utc::now();
|
||||||
|
let rows: Vec<(Lc, String, String)> = ZONES
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label_key, zone)| {
|
||||||
|
let tz: Tz = zone.parse().expect("valid IANA timezone");
|
||||||
|
let zone_cx = Context::default().with_langid(cx).with_timezone(tz);
|
||||||
|
(
|
||||||
|
Lc::t(label_key, &LOC),
|
||||||
|
zone_cx.format_datetime(now, DateFormat::Medium, TimeFormat::Short),
|
||||||
|
zone_cx.format_iso_datetime(now),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
ul {
|
||||||
|
@for (label, combined, iso) in &rows {
|
||||||
|
li {
|
||||||
|
strong { (label.using(cx)) ": " } (combined)
|
||||||
|
" (ISO " code { (iso) } ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combinaciones de `DateFormat`/`TimeFormat` sobre la zona horaria efectiva del visitante. Para
|
||||||
|
// añadir un nuevo formato basta con ampliar el array correspondiente.
|
||||||
|
fn formats_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("datetime_block_formats", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
let now = Utc::now();
|
||||||
|
let today = now.with_timezone(&cx.timezone()).date_naive();
|
||||||
|
|
||||||
|
let dates: Vec<(&str, String)> = [
|
||||||
|
("Short", DateFormat::Short),
|
||||||
|
("Medium", DateFormat::Medium),
|
||||||
|
("Long", DateFormat::Long),
|
||||||
|
("Iso", DateFormat::Iso),
|
||||||
|
("Custom(\"%d.%m.%Y\")", DateFormat::Custom("%d.%m.%Y")),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, format)| (label, cx.format_date(today, format)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let times: Vec<(&str, String)> = [
|
||||||
|
("Short", TimeFormat::Short),
|
||||||
|
("Long", TimeFormat::Long),
|
||||||
|
("Custom(\"%I:%M %p\")", TimeFormat::Custom("%I:%M %p")),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, format)| (label, cx.format_time(now, format)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let combos: Vec<(&str, String)> = [
|
||||||
|
("Short + Short", DateFormat::Short, TimeFormat::Short),
|
||||||
|
("Medium + Short", DateFormat::Medium, TimeFormat::Short),
|
||||||
|
("Long + Long", DateFormat::Long, TimeFormat::Long),
|
||||||
|
("Long + Short", DateFormat::Long, TimeFormat::Short),
|
||||||
|
("Iso + Long", DateFormat::Iso, TimeFormat::Long),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, date, time)| (label, cx.format_datetime(now, date, time)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
h3 { (Lc::t("datetime_formats_date", &LOC).using(cx)) }
|
||||||
|
ul {
|
||||||
|
@for (label, value) in &dates {
|
||||||
|
li { code { (label) } ": " (value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 { (Lc::t("datetime_formats_time", &LOC).using(cx)) }
|
||||||
|
ul {
|
||||||
|
@for (label, value) in × {
|
||||||
|
li { code { (label) } ": " (value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 { (Lc::t("datetime_formats_combos", &LOC).using(cx)) }
|
||||||
|
ul {
|
||||||
|
@for (label, value) in &combos {
|
||||||
|
li { code { (label) } ": " (value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
(Lc::t("datetime_formats_iso", &LOC).using(cx))
|
||||||
|
" " code { (cx.format_iso_datetime(now)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Casos ilustrativos de `RelativeFormat` sobre la zona horaria efectiva del visitante: hoy, un
|
||||||
|
// desplazamiento simple de días, uno con el componente de meses en cero, uno con los tres
|
||||||
|
// componentes y uno en el futuro -- siempre respecto al día actual. Añadir un caso nuevo es sólo
|
||||||
|
// ampliar `samples`. La primera columna muestra la fecha seleccionada (no una etiqueta fija),
|
||||||
|
// formateada con `DateFormat::Medium` en el idioma efectivo, para que se vea a qué fecha concreta
|
||||||
|
// corresponde cada resultado relativo.
|
||||||
|
fn relative_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("datetime_block_relative", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
|
||||||
|
// Desplazamientos de meses/años construidos con `checked_sub_months()` (mismo mecanismo
|
||||||
|
// que usa `RelativeFormat` por dentro), para que el resultado coincida exactamente con
|
||||||
|
// el criterio de cada caso.
|
||||||
|
let two_years_and_3_days_ago = today
|
||||||
|
.checked_sub_months(Months::new(24))
|
||||||
|
.expect("valid date")
|
||||||
|
- Duration::days(3);
|
||||||
|
let three_years_2_months_10_days_ago = today
|
||||||
|
.checked_sub_months(Months::new(38))
|
||||||
|
.expect("valid date")
|
||||||
|
- Duration::days(10);
|
||||||
|
|
||||||
|
let samples = [
|
||||||
|
today,
|
||||||
|
today - Duration::days(3),
|
||||||
|
two_years_and_3_days_ago,
|
||||||
|
three_years_2_months_10_days_ago,
|
||||||
|
today + Duration::days(5),
|
||||||
|
];
|
||||||
|
|
||||||
|
let rows: Vec<(String, String, String, String)> = samples
|
||||||
|
.into_iter()
|
||||||
|
.map(|date| {
|
||||||
|
let dt = at_noon(date);
|
||||||
|
(
|
||||||
|
cx.format_date(date, DateFormat::Medium),
|
||||||
|
cx.format_relative(dt, RelativeFormat::Short),
|
||||||
|
cx.format_relative(dt, RelativeFormat::Medium),
|
||||||
|
cx.format_relative(dt, RelativeFormat::Long),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
table style="width: 100%; border: 1px solid black;" {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { (Lc::t("datetime_relative_col_date", &LOC).using(cx)) }
|
||||||
|
th { "Short" }
|
||||||
|
th { "Medium" }
|
||||||
|
th { "Long" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for (date, short, medium, long) in &rows {
|
||||||
|
tr {
|
||||||
|
td { (date) }
|
||||||
|
td { (short) }
|
||||||
|
td { (medium) }
|
||||||
|
td { (long) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Niveles de `DatePrecision` para `format_since()`/`format_until()`. Añadir un nivel nuevo es sólo
|
||||||
|
// ampliar este array.
|
||||||
|
const PRECISIONS: [(&str, DatePrecision); 3] = [
|
||||||
|
("Short", DatePrecision::Short),
|
||||||
|
("Medium", DatePrecision::Medium),
|
||||||
|
("Long", DatePrecision::Long),
|
||||||
|
];
|
||||||
|
|
||||||
|
// `format_since()`/`format_until()` sobre una única fecha de ejemplo: a diferencia de `DateFormat`,
|
||||||
|
// lo que cambia entre niveles no es el estilo, sino la propia precisión revelada (sólo el mes, mes
|
||||||
|
// y año, o fecha completa).
|
||||||
|
fn since_until_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("datetime_block_since_until", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2026, 6, 3).unwrap();
|
||||||
|
|
||||||
|
let rows: Vec<(&str, String, String)> = PRECISIONS
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, precision)| {
|
||||||
|
(
|
||||||
|
label,
|
||||||
|
cx.format_since(date, precision),
|
||||||
|
cx.format_until(date, precision),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
table style="width: 100%; border: 1px solid black;" {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { (Lc::t("datetime_since_until_col_precision", &LOC).using(cx)) }
|
||||||
|
th { (Lc::t("datetime_since_until_col_since", &LOC).using(cx)) }
|
||||||
|
th { (Lc::t("datetime_since_until_col_until", &LOC).using(cx)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for (label, since, until) in &rows {
|
||||||
|
tr {
|
||||||
|
td { (*label) }
|
||||||
|
td { (since) }
|
||||||
|
td { (until) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mediodía, para evitar que el redondeo horario de la conversión de zona horaria empuje la fecha
|
||||||
|
// civil resultante al día anterior o siguiente en zonas con un offset amplio.
|
||||||
|
fn at_noon(date: NaiveDate) -> DateTime<Utc> {
|
||||||
|
date.and_hms_opt(12, 0, 0)
|
||||||
|
.expect("noon is always a valid time")
|
||||||
|
.and_utc()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroDatetime).await.run().await
|
||||||
|
}
|
||||||
548
examples/intro-flex.rs
Normal file
|
|
@ -0,0 +1,548 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroFlex;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroFlex {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_flex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_flex(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_assets(demo_box_styles())
|
||||||
|
.with_assets(demo_row_styles())
|
||||||
|
.with_child(
|
||||||
|
Intro::custom()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("flex_slogan", &LOC))
|
||||||
|
.with_child(direction_block())
|
||||||
|
.with_child(justify_block())
|
||||||
|
.with_child(align_block())
|
||||||
|
.with_child(align_self_block())
|
||||||
|
.with_child(align_content_block())
|
||||||
|
.with_child(grow_shrink_block())
|
||||||
|
.with_child(other_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direction_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_direction", &LOC));
|
||||||
|
|
||||||
|
let direction_variants: [(&str, Flex, &str); 4] = [
|
||||||
|
("flex_title_direction_row", Flex::new(), "Flex::new()"),
|
||||||
|
(
|
||||||
|
"flex_title_direction_row_reverse",
|
||||||
|
Flex::new().with_direction(flex::Direction::RowReverse),
|
||||||
|
"Flex::new().with_direction(Direction::RowReverse)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_direction_column",
|
||||||
|
Flex::new().with_direction(flex::Direction::Column),
|
||||||
|
"Flex::new().with_direction(Direction::Column)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_direction_column_reverse",
|
||||||
|
Flex::new().with_direction(flex::Direction::ColumnReverse),
|
||||||
|
"Flex::new().with_direction(Direction::ColumnReverse)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, flex, code) in direction_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_row(flex)
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn justify_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_justify", &LOC));
|
||||||
|
|
||||||
|
let justify_variants: [(&str, flex::ContentJustify, &str); 6] = [
|
||||||
|
(
|
||||||
|
"flex_title_justify_start",
|
||||||
|
flex::ContentJustify::Start,
|
||||||
|
"Flex::new().with_justify(ContentJustify::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_center",
|
||||||
|
flex::ContentJustify::Center,
|
||||||
|
"Flex::new().with_justify(ContentJustify::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_end",
|
||||||
|
flex::ContentJustify::End,
|
||||||
|
"Flex::new().with_justify(ContentJustify::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_between",
|
||||||
|
flex::ContentJustify::SpaceBetween,
|
||||||
|
"Flex::new().with_justify(ContentJustify::SpaceBetween)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_around",
|
||||||
|
flex::ContentJustify::SpaceAround,
|
||||||
|
"Flex::new().with_justify(ContentJustify::SpaceAround)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_justify_evenly",
|
||||||
|
flex::ContentJustify::SpaceEvenly,
|
||||||
|
"Flex::new().with_justify(ContentJustify::SpaceEvenly)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, justify, code) in justify_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_justify(justify)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align", &LOC));
|
||||||
|
|
||||||
|
let align_variants: [(&str, align::Items, &str); 4] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_start",
|
||||||
|
align::Items::Start,
|
||||||
|
"Flex::new().with_align(align::Items::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_center",
|
||||||
|
align::Items::Center,
|
||||||
|
"Flex::new().with_align(align::Items::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_end",
|
||||||
|
align::Items::End,
|
||||||
|
"Flex::new().with_align(align::Items::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_stretch",
|
||||||
|
align::Items::Stretch,
|
||||||
|
"Flex::new().with_align(align::Items::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align, code) in align_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem"))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_medium", &LOC)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_short", &LOC), "0.15rem 1rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_align_baseline", &LOC),
|
||||||
|
Lc::n("Flex::new().with_align(align::Items::Baseline)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Baseline)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("font-size", "1.75rem")),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_medium", &LOC)))
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_short", &LOC), "0.15rem 1rem")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_self_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align_self", &LOC));
|
||||||
|
|
||||||
|
let align_self_variants: [(&str, align::ItemSelf, &str); 4] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_self_start",
|
||||||
|
align::ItemSelf::Start,
|
||||||
|
"FlexItem::new().with_align_self(align::ItemSelf::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_end",
|
||||||
|
align::ItemSelf::End,
|
||||||
|
"FlexItem::new().with_align_self(align::ItemSelf::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_center",
|
||||||
|
align::ItemSelf::Center,
|
||||||
|
"FlexItem::new().with_align_self(align::ItemSelf::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_self_stretch",
|
||||||
|
align::ItemSelf::Stretch,
|
||||||
|
"FlexItem::new().with_align_self(align::ItemSelf::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align_self, code) in align_self_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Start)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem"))
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("1")).with_prop(FlexItem::new().with_align_self(align_self)),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_align_self_baseline", &LOC),
|
||||||
|
Lc::n("FlexItem::new().with_align_self(align::ItemSelf::Baseline)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Start)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("font-size", "1.75rem")),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("1"))
|
||||||
|
.with_prop(FlexItem::new().with_align_self(align::ItemSelf::Baseline)),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(Lc::t("flex_box_tall", &LOC), "2.5rem 1rem")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align_content_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_align_content", &LOC));
|
||||||
|
|
||||||
|
let align_content_variants: [(&str, align::Content, &str); 7] = [
|
||||||
|
(
|
||||||
|
"flex_title_align_content_start",
|
||||||
|
align::Content::Start,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_end",
|
||||||
|
align::Content::End,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::End)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_center",
|
||||||
|
align::Content::Center,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_between",
|
||||||
|
align::Content::SpaceBetween,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceBetween)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_around",
|
||||||
|
align::Content::SpaceAround,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceAround)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_evenly",
|
||||||
|
align::Content::SpaceEvenly,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::SpaceEvenly)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"flex_title_align_content_stretch",
|
||||||
|
align::Content::Stretch,
|
||||||
|
"Flex::new().with_wrap(Behavior::Wrap).with_align_content(align::Content::Stretch)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, align_content, code) in align_content_variants {
|
||||||
|
let mut row = demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_align_content(align_content)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "21rem"))
|
||||||
|
.with_prop(PropsOp::add_style("min-height", "11rem"));
|
||||||
|
for label in ["1", "2", "3", "4"] {
|
||||||
|
row = row.with_child(
|
||||||
|
sized_box(flex_item(label), "0.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("width", "9rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(row);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow_shrink_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("flex_block_title_grow_shrink", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grow", &LOC),
|
||||||
|
Lc::n("FlexItem::new().with_grow(flex::ItemGrow::Is1)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_fixed", &LOC)))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("flex_box_grows", &LOC))
|
||||||
|
.with_prop(FlexItem::new().with_grow(flex::ItemGrow::Is1)),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_fixed", &LOC))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_shrink", &LOC),
|
||||||
|
Lc::n("FlexItem::new().with_shrink(flex::ItemShrink::Is0)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "31rem"))
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("1")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("2"))
|
||||||
|
.with_prop(PropsOp::add_style("width", "10.5rem"))
|
||||||
|
.with_prop(FlexItem::new().with_shrink(flex::ItemShrink::Is0)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(flex_item("3")).with_prop(PropsOp::add_style("width", "10.5rem")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn other_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("flex_block_title_other", &LOC));
|
||||||
|
|
||||||
|
block = block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_push_end", &LOC),
|
||||||
|
Lc::n("FlexItem::push_end()"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_start_1", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_start_2", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_end", &LOC)).with_prop(FlexItem::push_end())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut wrap_row = demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_wrap(flex::Behavior::Wrap)
|
||||||
|
.with_align_content(align::Content::SpaceBetween)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "20rem"))
|
||||||
|
.with_prop(PropsOp::add_style("min-height", "11rem"));
|
||||||
|
for label in ["1", "2", "3", "4", "5", "6", "7", "8"] {
|
||||||
|
wrap_row = wrap_row.with_child(
|
||||||
|
sized_box(flex_item(label), "0.5rem 1rem")
|
||||||
|
.with_prop(PropsOp::add_style("width", "4rem")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_wrap", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_wrap(Behavior::Wrap)",
|
||||||
|
".with_align_content(align::Content::SpaceBetween)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(wrap_row)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_order", &LOC),
|
||||||
|
Lc::n("FlexItem::new().with_order(ItemOrder::First) / .with_order(ItemOrder::Last)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::n("A"))
|
||||||
|
.with_prop(FlexItem::new().with_order(flex::ItemOrder::Last)),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::n("B")))
|
||||||
|
.with_child(demo_box(Lc::n("C")))
|
||||||
|
.with_child(demo_box(Lc::n("D")))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::n("E"))
|
||||||
|
.with_prop(FlexItem::new().with_order(flex::ItemOrder::First)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_gap_none", &LOC),
|
||||||
|
Lc::n("Flex::new() (align::Gap::None por defecto)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new())
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_gap_some", &LOC),
|
||||||
|
Lc::n("Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(1.5)))"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(1.5))))
|
||||||
|
.with_child(demo_box(flex_item("1")))
|
||||||
|
.with_child(demo_box(flex_item("2")))
|
||||||
|
.with_child(demo_box(flex_item("3"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grid_thirds", &LOC),
|
||||||
|
Lc::n("FlexItem::new().with_size(flex::ItemSize::Percent33)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new())
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::n("1/3"))
|
||||||
|
.with_prop(FlexItem::new().with_size(flex::ItemSize::Percent33)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::n("1/3"))
|
||||||
|
.with_prop(FlexItem::new().with_size(flex::ItemSize::Percent33)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::n("1/3"))
|
||||||
|
.with_prop(FlexItem::new().with_size(flex::ItemSize::Percent33)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_grid_offset", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"FlexItem::new()",
|
||||||
|
".with_size(ItemSize::Percent50)",
|
||||||
|
".with_offset(ItemOffset::Percent25)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new()).with_child(
|
||||||
|
demo_box(Lc::t("flex_box_half_centered", &LOC)).with_prop(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_size(flex::ItemSize::Percent50)
|
||||||
|
.with_offset(flex::ItemOffset::Percent25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("flex_title_toolbar", &LOC),
|
||||||
|
Lc::t("flex_desc_toolbar", &LOC),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Center)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_file", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_edit", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_view", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_profile", &LOC)).with_prop(FlexItem::push_end()))
|
||||||
|
.with_child(demo_box(Lc::t("flex_box_logout", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Aspecto fijo de las cajas de muestra.
|
||||||
|
fn demo_box_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"flex-demo-box",
|
||||||
|
[
|
||||||
|
("background-color", "#0d6efd"),
|
||||||
|
("color", "#fff"),
|
||||||
|
("min-width", "3rem"),
|
||||||
|
("width", "auto"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0"),
|
||||||
|
("border-radius", "0.375rem"),
|
||||||
|
("text-align", "center"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspecto fijo de las filas de muestra.
|
||||||
|
fn demo_row_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"flex-demo-row",
|
||||||
|
[
|
||||||
|
("background-color", "#f1f3f5"),
|
||||||
|
("width", "100%"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
("padding", "0.75rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||||
|
fn sized_box(label: Lc, padding: &'static str) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("flex-demo-box"))
|
||||||
|
.with_prop(PropsOp::add_style("padding", padding))
|
||||||
|
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con el relleno vertical estandar del resto de ejemplos.
|
||||||
|
fn demo_box(label: Lc) -> Container {
|
||||||
|
sized_box(label, "0.5rem 1rem")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta "Flex item N" para las cajas que solo se distinguen por su posicion.
|
||||||
|
fn flex_item(n: impl Into<CowStr>) -> Lc {
|
||||||
|
Lc::t("flex_item_label", &LOC).with_arg("n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila de demostracion con fondo gris para visualizar los limites del propio contenedor flex.
|
||||||
|
fn demo_row(flex: Flex) -> Flex {
|
||||||
|
flex.with_prop(PropsOp::add_classes("flex-demo-row"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titulo y fragmento de codigo que introducen cada demostracion.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroFlex).await.run().await
|
||||||
|
}
|
||||||
543
examples/intro-grid.rs
Normal file
|
|
@ -0,0 +1,543 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroGrid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroGrid {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_grid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_grid(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_assets(demo_box_styles())
|
||||||
|
.with_assets(demo_row_styles())
|
||||||
|
.with_child(
|
||||||
|
Intro::custom()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("grid_slogan", &LOC))
|
||||||
|
.with_child(columns_block())
|
||||||
|
.with_child(rows_gap_block())
|
||||||
|
.with_child(placement_block())
|
||||||
|
.with_child(alignment_block())
|
||||||
|
.with_child(content_alignment_block())
|
||||||
|
.with_child(item_alignment_block())
|
||||||
|
.with_child(auto_flow_block())
|
||||||
|
.with_child(auto_tracks_block())
|
||||||
|
.with_child(responsive_block())
|
||||||
|
.with_child(layout_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn columns_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_columns", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_columns_thirds", &LOC),
|
||||||
|
Lc::n("Grid::new().with_columns(Tracks::repeat(3, AxisTrack::Fraction(1.0)))"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_columns_sidebar", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new().with_columns(Tracks::new()",
|
||||||
|
".with_track(AxisTrack::Fixed(UnitValue::Px(140)))",
|
||||||
|
".with_track(AxisTrack::Fraction(1.0)))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(
|
||||||
|
grid::Tracks::new()
|
||||||
|
.with_track(grid::AxisTrack::Fixed(UnitValue::Px(140)))
|
||||||
|
.with_track(grid::AxisTrack::Fraction(1.0)),
|
||||||
|
)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_sidebar", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_content", &LOC))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_columns_min_max", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new().with_columns(Tracks::new()",
|
||||||
|
".with_track(AxisTrack::MinContent)",
|
||||||
|
".with_track(AxisTrack::Fraction(1.0))",
|
||||||
|
".with_track(AxisTrack::MaxContent))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(
|
||||||
|
grid::Tracks::new()
|
||||||
|
.with_track(grid::AxisTrack::MinContent)
|
||||||
|
.with_track(grid::AxisTrack::Fraction(1.0))
|
||||||
|
.with_track(grid::AxisTrack::MaxContent),
|
||||||
|
)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_min_content", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_fraction", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_max_content", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rows_gap_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_rows_gap", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_rows", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new()",
|
||||||
|
".with_columns(Tracks::repeat(2, AxisTrack::Fraction(1.0)))",
|
||||||
|
".with_rows(Tracks::repeat(2, AxisTrack::Fixed(UnitValue::RelRem(3.0))))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_rows(grid::Tracks::repeat(
|
||||||
|
2,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::RelRem(3.0)),
|
||||||
|
))
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3")))
|
||||||
|
.with_child(demo_box(grid_box("4"))),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_gap_distinct", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new().with_gap(align::Gap::Distinct {",
|
||||||
|
" row: UnitValue::RelRem(1.5), column: UnitValue::RelRem(0.25) })",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_gap(align::Gap::Distinct {
|
||||||
|
row: UnitValue::RelRem(1.5),
|
||||||
|
column: UnitValue::RelRem(0.25),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3")))
|
||||||
|
.with_child(demo_box(grid_box("4")))
|
||||||
|
.with_child(demo_box(grid_box("5")))
|
||||||
|
.with_child(demo_box(grid_box("6"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placement_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_placement", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_placement", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"GridItem::new().with_column(ItemPlacement::Span(2))",
|
||||||
|
" / .with_column(ItemPlacement::Range(2, 4))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(4, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_plain", &LOC)))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_span_2", &LOC))
|
||||||
|
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_plain", &LOC)))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_range", &LOC)).with_prop(
|
||||||
|
GridItem::new()
|
||||||
|
.with_column(grid::ItemPlacement::Range(2, 4))
|
||||||
|
.with_row(grid::ItemPlacement::Line(2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alignment_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("grid_block_title_alignment", &LOC));
|
||||||
|
|
||||||
|
let variants: [(&str, grid::DefaultJustify, align::Items, &str); 4] = [
|
||||||
|
(
|
||||||
|
"grid_title_alignment_start",
|
||||||
|
grid::DefaultJustify::Start,
|
||||||
|
align::Items::Start,
|
||||||
|
concat!(
|
||||||
|
"Grid::new().with_justify_items(DefaultJustify::Start)",
|
||||||
|
".with_align_items(align::Items::Start)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_alignment_center",
|
||||||
|
grid::DefaultJustify::Center,
|
||||||
|
align::Items::Center,
|
||||||
|
concat!(
|
||||||
|
"Grid::new().with_justify_items(DefaultJustify::Center)",
|
||||||
|
".with_align_items(align::Items::Center)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_alignment_end",
|
||||||
|
grid::DefaultJustify::End,
|
||||||
|
align::Items::End,
|
||||||
|
concat!(
|
||||||
|
"Grid::new().with_justify_items(DefaultJustify::End)",
|
||||||
|
".with_align_items(align::Items::End)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_alignment_stretch",
|
||||||
|
grid::DefaultJustify::Stretch,
|
||||||
|
align::Items::Stretch,
|
||||||
|
concat!(
|
||||||
|
"Grid::new().with_justify_items(DefaultJustify::Stretch)",
|
||||||
|
".with_align_items(align::Items::Stretch)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, justify_items, align_items, code) in variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(
|
||||||
|
3,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::Px(96)),
|
||||||
|
))
|
||||||
|
.with_rows(grid::Tracks::repeat(
|
||||||
|
1,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::RelRem(4.5)),
|
||||||
|
))
|
||||||
|
.with_justify_items(justify_items)
|
||||||
|
.with_align_items(align_items)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_alignment_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("grid_block_title_content_alignment", &LOC));
|
||||||
|
|
||||||
|
let variants: [(&str, grid::ContentJustify, &str); 4] = [
|
||||||
|
(
|
||||||
|
"grid_title_content_alignment_start",
|
||||||
|
grid::ContentJustify::Start,
|
||||||
|
"Grid::new().with_justify_content(ContentJustify::Start)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_content_alignment_center",
|
||||||
|
grid::ContentJustify::Center,
|
||||||
|
"Grid::new().with_justify_content(ContentJustify::Center)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_content_alignment_between",
|
||||||
|
grid::ContentJustify::SpaceBetween,
|
||||||
|
"Grid::new().with_justify_content(ContentJustify::SpaceBetween)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"grid_title_content_alignment_evenly",
|
||||||
|
grid::ContentJustify::SpaceEvenly,
|
||||||
|
"Grid::new().with_justify_content(ContentJustify::SpaceEvenly)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, justify_content, code) in variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(
|
||||||
|
3,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::Px(72)),
|
||||||
|
))
|
||||||
|
.with_justify_content(justify_content)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("max-width", "26rem"))
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_alignment_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_item_alignment", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_item_alignment", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"GridItem::new()",
|
||||||
|
".with_justify_self(ItemJustify::End)",
|
||||||
|
".with_align_self(align::ItemSelf::End)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(
|
||||||
|
3,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::Px(96)),
|
||||||
|
))
|
||||||
|
.with_rows(grid::Tracks::repeat(
|
||||||
|
1,
|
||||||
|
grid::AxisTrack::Fixed(UnitValue::RelRem(4.5)),
|
||||||
|
))
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_overridden", &LOC)).with_prop(
|
||||||
|
GridItem::new()
|
||||||
|
.with_justify_self(grid::ItemJustify::End)
|
||||||
|
.with_align_self(align::ItemSelf::End),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("3"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_flow_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_auto_flow", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_auto_flow_row", &LOC),
|
||||||
|
Lc::n("Grid::new().with_auto_flow(AutoFlow::Row) (por defecto)"),
|
||||||
|
))
|
||||||
|
.with_child(dense_demo(grid::AutoFlow::Row))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_auto_flow_dense", &LOC),
|
||||||
|
Lc::n("Grid::new().with_auto_flow(AutoFlow::RowDense)"),
|
||||||
|
))
|
||||||
|
.with_child(dense_demo(grid::AutoFlow::RowDense))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dense_demo(auto_flow: grid::AutoFlow) -> Grid {
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(4, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_auto_flow(auto_flow)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_span_2", &LOC))
|
||||||
|
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_span_2", &LOC))
|
||||||
|
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(2))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_tracks_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_auto_tracks", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_auto_tracks", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new()",
|
||||||
|
".with_columns(Tracks::repeat(3, AxisTrack::Fraction(1.0)))",
|
||||||
|
".with_auto_rows(AxisTrack::Fixed(UnitValue::RelRem(3.0)))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_auto_rows(grid::AxisTrack::Fixed(UnitValue::RelRem(3.0)))
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(grid_box("1")))
|
||||||
|
.with_child(demo_box(grid_box("2")))
|
||||||
|
.with_child(demo_box(grid_box("3")))
|
||||||
|
.with_child(demo_box(grid_box("4")))
|
||||||
|
.with_child(demo_box(grid_box("5")))
|
||||||
|
.with_child(demo_box(grid_box("6"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn responsive_block() -> Block {
|
||||||
|
let mut row = demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(grid::Tracks::repeat(1, grid::AxisTrack::Fraction(1.0)))
|
||||||
|
.with_columns_at(
|
||||||
|
Breakpoint::Sm,
|
||||||
|
grid::Tracks::repeat(2, grid::AxisTrack::Fraction(1.0)),
|
||||||
|
)
|
||||||
|
.with_columns_at(
|
||||||
|
Breakpoint::Md,
|
||||||
|
grid::Tracks::repeat(3, grid::AxisTrack::Fraction(1.0)),
|
||||||
|
)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
);
|
||||||
|
for n in 1..=6 {
|
||||||
|
row = row.with_child(demo_box(card_label(n)));
|
||||||
|
}
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_responsive", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_responsive", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Grid::new()",
|
||||||
|
".with_columns(Tracks::repeat(1, AxisTrack::Fraction(1.0)))",
|
||||||
|
".with_columns_at(Breakpoint::Sm, Tracks::repeat(2, AxisTrack::Fraction(1.0)))",
|
||||||
|
".with_columns_at(Breakpoint::Md, Tracks::repeat(3, AxisTrack::Fraction(1.0)))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("grid_block_title_layout", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("grid_title_layout", &LOC),
|
||||||
|
Lc::t("grid_desc_layout", &LOC),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_grid(
|
||||||
|
Grid::new()
|
||||||
|
.with_columns(
|
||||||
|
grid::Tracks::new()
|
||||||
|
.with_track(grid::AxisTrack::Fixed(UnitValue::Px(120)))
|
||||||
|
.with_track(grid::AxisTrack::Fraction(1.0))
|
||||||
|
.with_track(grid::AxisTrack::Fixed(UnitValue::Px(120))),
|
||||||
|
)
|
||||||
|
.with_rows(
|
||||||
|
grid::Tracks::new()
|
||||||
|
.with_track(grid::AxisTrack::Auto)
|
||||||
|
.with_track(grid::AxisTrack::Fraction(1.0))
|
||||||
|
.with_track(grid::AxisTrack::Auto),
|
||||||
|
)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_style("min-height", "18rem"))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_header", &LOC))
|
||||||
|
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_nav", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_content", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("grid_box_aside", &LOC)))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("grid_box_footer", &LOC))
|
||||||
|
.with_prop(GridItem::new().with_column(grid::ItemPlacement::Span(3))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Aspecto fijo de las cajas de muestra: esquinas rectas y borde claro, a diferencia de las cajas
|
||||||
|
// redondeadas de `intro-flex.rs`, para distinguir a simple vista los ejemplos de Grid de los de
|
||||||
|
// Flex.
|
||||||
|
fn demo_box_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"grid-demo-box",
|
||||||
|
[
|
||||||
|
("background-color", "#0d6efd"),
|
||||||
|
("color", "#fff"),
|
||||||
|
("border", "2px solid #6ea8fe"),
|
||||||
|
("min-width", "3rem"),
|
||||||
|
("width", "auto"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0"),
|
||||||
|
("text-align", "center"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspecto fijo de las filas de muestra.
|
||||||
|
fn demo_row_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"grid-demo-row",
|
||||||
|
[
|
||||||
|
("background-color", "#f1f3f5"),
|
||||||
|
("width", "100%"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
("padding", "0.75rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con el relleno vertical estandar del resto de ejemplos.
|
||||||
|
fn demo_box(label: Lc) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("grid-demo-box"))
|
||||||
|
.with_prop(PropsOp::add_style("padding", "0.5rem 1rem"))
|
||||||
|
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta "Box N" para las cajas que sólo se distinguen por su posición.
|
||||||
|
fn grid_box(n: impl Into<CowStr>) -> Lc {
|
||||||
|
Lc::t("grid_box_label", &LOC).with_arg("n", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta "Tarjeta N" para las cajas de la rejilla responsive.
|
||||||
|
fn card_label(n: usize) -> Lc {
|
||||||
|
Lc::t("grid_box_card", &LOC).with_arg("n", n.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rejilla de demostracion con fondo gris para visualizar los limites del propio contenedor grid.
|
||||||
|
fn demo_grid(grid: Grid) -> Grid {
|
||||||
|
grid.with_prop(PropsOp::add_classes("grid-demo-row"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titulo y fragmento de codigo que introducen cada demostracion.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroGrid).await.run().await
|
||||||
|
}
|
||||||
263
examples/intro-responsive.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroResponsive;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroResponsive {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_responsive))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_responsive(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_assets(demo_box_styles())
|
||||||
|
.with_assets(demo_row_styles())
|
||||||
|
.with_assets(demo_code_styles())
|
||||||
|
.with_child(
|
||||||
|
Intro::custom()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("responsive_slogan", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p class="intro-text-lead" {
|
||||||
|
(Lc::t("responsive_note", &LOC).using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.with_child(activation_block())
|
||||||
|
.with_child(direction_block())
|
||||||
|
.with_child(grid_block())
|
||||||
|
.with_child(justify_align_block())
|
||||||
|
.with_child(order_block())
|
||||||
|
.with_child(gap_grow_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activation_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_activation", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_activation", &LOC),
|
||||||
|
Lc::n("Flex::at(Breakpoint::Md)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::at(Breakpoint::Md).with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_home", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_products", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_about", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_nav_contact", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direction_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_direction", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_direction", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_direction(Direction::Column)",
|
||||||
|
".with_direction_at(Breakpoint::Md, Direction::RowReverse)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_direction(flex::Direction::Column)
|
||||||
|
.with_direction_at(Breakpoint::Md, flex::Direction::RowReverse)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(sized_box(
|
||||||
|
Lc::t("responsive_box_image", &LOC),
|
||||||
|
"2.5rem 1rem",
|
||||||
|
))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_text", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_block() -> Block {
|
||||||
|
let mut row = demo_row(Flex::new().with_wrap(flex::Behavior::Wrap));
|
||||||
|
for n in 1..=6 {
|
||||||
|
row = row.with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_style("padding", "0.25rem"))
|
||||||
|
.with_prop(
|
||||||
|
FlexItem::new()
|
||||||
|
.with_size(flex::ItemSize::Percent100)
|
||||||
|
.with_size_at(Breakpoint::Sm, flex::ItemSize::Percent50)
|
||||||
|
.with_size_at(Breakpoint::Md, flex::ItemSize::Percent33),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(card_label(n))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_grid", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_grid", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"FlexItem::new()",
|
||||||
|
".with_size(ItemSize::Percent100)",
|
||||||
|
".with_size_at(Breakpoint::Sm, ItemSize::Percent50)",
|
||||||
|
".with_size_at(Breakpoint::Md, ItemSize::Percent33)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn justify_align_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_justify_align", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_justify_align", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_justify(ContentJustify::Center)",
|
||||||
|
".with_justify_at(Breakpoint::Md, ContentJustify::SpaceBetween)",
|
||||||
|
".with_align(align::Items::Center)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_justify(flex::ContentJustify::Center)
|
||||||
|
.with_justify_at(Breakpoint::Md, flex::ContentJustify::SpaceBetween)
|
||||||
|
.with_align(align::Items::Center)
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_logo", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_menu", &LOC))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn order_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_order", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_order", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::at(Breakpoint::Lg)",
|
||||||
|
" + FlexItem::new().with_order_at(Breakpoint::Lg, ItemOrder::First)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::at(Breakpoint::Lg).with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_content", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_sidebar", &LOC)).with_prop(
|
||||||
|
FlexItem::new().with_order_at(Breakpoint::Lg, flex::ItemOrder::First),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gap_grow_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("responsive_block_title_gap_grow", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("responsive_title_gap_grow", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Flex::new()",
|
||||||
|
".with_gap(align::Gap::Both(RelRem(0.5)))",
|
||||||
|
".with_gap_at(Breakpoint::Md, Gap::Both(RelRem(1.5)))",
|
||||||
|
" + FlexItem::new().with_grow_at(Breakpoint::Md, ItemGrow::Is1)",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(
|
||||||
|
Flex::new()
|
||||||
|
.with_gap(align::Gap::Both(UnitValue::RelRem(0.5)))
|
||||||
|
.with_gap_at(Breakpoint::Md, align::Gap::Both(UnitValue::RelRem(1.5))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_file", &LOC)))
|
||||||
|
.with_child(demo_box(Lc::t("responsive_box_edit", &LOC)))
|
||||||
|
.with_child(
|
||||||
|
demo_box(Lc::t("responsive_box_search", &LOC))
|
||||||
|
.with_prop(FlexItem::new().with_grow_at(Breakpoint::Md, flex::ItemGrow::Is1)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Aspecto fijo de las cajas de muestra.
|
||||||
|
fn demo_box_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"flex-demo-box",
|
||||||
|
[
|
||||||
|
("background-color", "#0d6efd"),
|
||||||
|
("color", "#fff"),
|
||||||
|
("min-width", "3rem"),
|
||||||
|
("width", "auto"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0"),
|
||||||
|
("border-radius", "0.375rem"),
|
||||||
|
("text-align", "center"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspecto fijo de las filas de muestra.
|
||||||
|
fn demo_row_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"flex-demo-row",
|
||||||
|
[
|
||||||
|
("background-color", "#f1f3f5"),
|
||||||
|
("width", "100%"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
("padding", "0.75rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita que los fragmentos de código largos desborden su contenedor.
|
||||||
|
fn demo_code_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(None, "flex-demo-code", [("overflow-wrap", "anywhere")])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con fondo azul y relleno vertical configurable, para mostrar diferencias de altura.
|
||||||
|
fn sized_box(label: Lc, padding: &'static str) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("flex-demo-box"))
|
||||||
|
.with_prop(PropsOp::add_style("padding", padding))
|
||||||
|
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja con el relleno vertical estandar del resto de ejemplos.
|
||||||
|
fn demo_box(label: Lc) -> Container {
|
||||||
|
sized_box(label, "0.5rem 1rem")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta "Tarjeta N" para las cajas de la rejilla responsive.
|
||||||
|
fn card_label(n: usize) -> Lc {
|
||||||
|
Lc::t("responsive_box_card", &LOC).with_arg("n", n.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila de demostracion con fondo gris para visualizar los limites del propio contenedor flex.
|
||||||
|
fn demo_row(flex: Flex) -> Flex {
|
||||||
|
flex.with_prop(PropsOp::add_classes("flex-demo-row"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titulo y fragmento de codigo que introducen cada demostracion.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code class="flex-demo-code" { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroResponsive).await.run().await
|
||||||
|
}
|
||||||
247
examples/intro-spacing.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
|
struct IntroSpacing;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for IntroSpacing {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/", web::get(intro_spacing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn intro_spacing(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_assets(demo_box_styles())
|
||||||
|
.with_assets(demo_row_styles())
|
||||||
|
.with_assets(demo_code_styles())
|
||||||
|
.with_child(
|
||||||
|
Intro::custom()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("spacing_slogan", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p class="intro-text-lead" {
|
||||||
|
(Lc::t("spacing_note", &LOC).using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.with_child(padding_block())
|
||||||
|
.with_child(layout_block())
|
||||||
|
.with_child(responsive_block())
|
||||||
|
.with_child(combined_block()),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn padding_block() -> Block {
|
||||||
|
let mut block = Block::new().with_title(Lc::t("spacing_block_title_padding", &LOC));
|
||||||
|
|
||||||
|
let padding_variants: [(&str, UnitValue, &str); 3] = [
|
||||||
|
(
|
||||||
|
"spacing_title_uniform_small",
|
||||||
|
UnitValue::RelRem(0.5),
|
||||||
|
"Padding::new().with_all(UnitValue::RelRem(0.5))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"spacing_title_uniform_medium",
|
||||||
|
UnitValue::RelRem(1.0),
|
||||||
|
"Padding::new().with_all(UnitValue::RelRem(1.0))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"spacing_title_uniform_large",
|
||||||
|
UnitValue::RelRem(2.0),
|
||||||
|
"Padding::new().with_all(UnitValue::RelRem(2.0))",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (title_key, size, code) in padding_variants {
|
||||||
|
block = block
|
||||||
|
.with_child(caption(Lc::t(title_key, &LOC), Lc::n(code)))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new().with_gap(align::Gap::Both(UnitValue::RelRem(0.5))))
|
||||||
|
.with_child(demo_box(box_sample()).with_prop(Padding::new().with_all(size))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("spacing_title_sides", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Padding::new()",
|
||||||
|
".with_top(UnitValue::RelRem(0.25))",
|
||||||
|
".with_end(UnitValue::RelRem(2.5))",
|
||||||
|
".with_bottom(UnitValue::RelRem(1.5))",
|
||||||
|
".with_start(UnitValue::RelRem(0.5))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new()).with_child(
|
||||||
|
demo_box(box_sample()).with_prop(
|
||||||
|
Padding::new()
|
||||||
|
.with_top(UnitValue::RelRem(0.25))
|
||||||
|
.with_end(UnitValue::RelRem(2.5))
|
||||||
|
.with_bottom(UnitValue::RelRem(1.5))
|
||||||
|
.with_start(UnitValue::RelRem(0.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("spacing_block_title_layout", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("spacing_title_margin_gap", &LOC),
|
||||||
|
Lc::n("Margin::new().with_x(UnitValue::RelRem(1.0))"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new())
|
||||||
|
.with_child(demo_box(box_sample()))
|
||||||
|
.with_child(
|
||||||
|
demo_box(box_sample()).with_prop(Margin::new().with_x(UnitValue::RelRem(1.0))),
|
||||||
|
)
|
||||||
|
.with_child(demo_box(box_sample())),
|
||||||
|
)
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("spacing_title_margin_center", &LOC),
|
||||||
|
Lc::n("Margin::new().with_x(UnitValue::Auto)"),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new()).with_child(
|
||||||
|
demo_box(box_sample())
|
||||||
|
.with_prop(Margin::new().with_x(UnitValue::Auto))
|
||||||
|
.with_prop(
|
||||||
|
FlexItem::new().with_size(flex::ItemSize::Custom(UnitValue::RelRem(8.0))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn responsive_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("spacing_block_title_responsive", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("spacing_title_responsive", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Padding::new()",
|
||||||
|
".with_all(UnitValue::RelRem(0.5))",
|
||||||
|
".with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))",
|
||||||
|
".with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new()).with_child(
|
||||||
|
demo_box(box_sample()).with_prop(
|
||||||
|
Padding::new()
|
||||||
|
.with_all(UnitValue::RelRem(0.5))
|
||||||
|
.with_all_at(Breakpoint::Md, UnitValue::RelRem(2.0))
|
||||||
|
.with_all_at(Breakpoint::Lg, UnitValue::RelRem(4.0)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combined_block() -> Block {
|
||||||
|
Block::new()
|
||||||
|
.with_title(Lc::t("spacing_block_title_combined", &LOC))
|
||||||
|
.with_child(caption(
|
||||||
|
Lc::t("spacing_title_combined", &LOC),
|
||||||
|
Lc::n(concat!(
|
||||||
|
"Container::new()",
|
||||||
|
".with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)))",
|
||||||
|
".with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)))",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
demo_row(Flex::new()).with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("spacing-demo-box"))
|
||||||
|
.with_prop(Margin::new().with_y(UnitValue::RelRem(1.0)))
|
||||||
|
.with_prop(Padding::new().with_all(UnitValue::RelRem(1.5)))
|
||||||
|
.with_child(
|
||||||
|
Button::plain(Lc::t("spacing_box_card_button", &LOC))
|
||||||
|
.with_style(button::Style::Solid(Intent::Warning)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Caja de muestra sin relleno ni margen propios, para que sólo se vea el efecto de `Margin`/
|
||||||
|
// `Padding` aplicado en cada demostración.
|
||||||
|
fn demo_box_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"spacing-demo-box",
|
||||||
|
[
|
||||||
|
("background-color", "#0d6efd"),
|
||||||
|
("color", "#fff"),
|
||||||
|
("min-width", "3rem"),
|
||||||
|
("width", "auto"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0"),
|
||||||
|
("padding", "0"),
|
||||||
|
("border-radius", "0.375rem"),
|
||||||
|
("text-align", "center"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspecto fijo de las filas de muestra.
|
||||||
|
fn demo_row_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"spacing-demo-row",
|
||||||
|
[
|
||||||
|
("background-color", "#f1f3f5"),
|
||||||
|
("width", "100%"),
|
||||||
|
("max-width", "none"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
("padding", "0.75rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita que los fragmentos de código largos desborden su contenedor.
|
||||||
|
fn demo_code_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(None, "spacing-demo-code", [("overflow-wrap", "anywhere")])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caja azul de muestra, sin relleno ni margen propios.
|
||||||
|
fn demo_box(label: Lc) -> Container {
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("spacing-demo-box"))
|
||||||
|
.with_child(Html::with(move |cx| html! { (label.using(cx)) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etiqueta genérica reutilizada en la mayoría de cajas de muestra.
|
||||||
|
fn box_sample() -> Lc {
|
||||||
|
Lc::t("spacing_box_sample", &LOC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fila de demostración con fondo gris para visualizar los límites de cada caja.
|
||||||
|
fn demo_row(flex: Flex) -> Flex {
|
||||||
|
flex.with_prop(PropsOp::add_classes("spacing-demo-row"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Título y fragmento de código que introducen cada demostración.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 { (title.using(cx)) }
|
||||||
|
p { code class="spacing-demo-code" { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
Application::prepare(&IntroSpacing).await.run().await
|
||||||
|
}
|
||||||
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
|
||||||
|
|
@ -2,7 +2,9 @@ title = Form controls
|
||||||
slogan = Bootsier form components showcase
|
slogan = Bootsier form components showcase
|
||||||
block_selections = Checkboxes, switches and radio buttons
|
block_selections = Checkboxes, switches and radio buttons
|
||||||
block_text = Text fields, multiline and range
|
block_text = Text fields, multiline and range
|
||||||
block_lists = Select lists and floating labels
|
block_lists = Selection lists
|
||||||
|
block_lists_floating = Select lists and floating labels
|
||||||
|
block_dialog = Modal dialog
|
||||||
|
|
||||||
fieldset_text = Text fields
|
fieldset_text = Text fields
|
||||||
label_name = Full name
|
label_name = Full name
|
||||||
|
|
@ -16,6 +18,8 @@ label_url = Website
|
||||||
placeholder_url = https://example.com
|
placeholder_url = https://example.com
|
||||||
label_search = Search
|
label_search = Search
|
||||||
placeholder_search = Search term...
|
placeholder_search = Search term...
|
||||||
|
help_name = Enter your full name as it appears on your ID.
|
||||||
|
help_email = We will only use your email to send important notifications.
|
||||||
|
|
||||||
fieldset_textarea = Multiline text
|
fieldset_textarea = Multiline text
|
||||||
label_comment = Comment
|
label_comment = Comment
|
||||||
|
|
@ -72,3 +76,8 @@ help_rating = From 1 (very poor) to 10 (excellent).
|
||||||
btn_submit = Submit
|
btn_submit = Submit
|
||||||
btn_reset = Reset
|
btn_reset = Reset
|
||||||
btn_cancel = Cancel
|
btn_cancel = Cancel
|
||||||
|
btn_ok = Ok
|
||||||
|
btn_delete = Delete record
|
||||||
|
|
||||||
|
dialog_delete_title = Delete record
|
||||||
|
dialog_delete_body = Are you sure you want to delete this record? This action cannot be undone.
|
||||||
|
|
|
||||||
21
examples/locale/en-US/intro-datetime.ftl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
datetime_slogan = Timezones and date/time formats
|
||||||
|
|
||||||
|
datetime_block_world = The same instant, around the world
|
||||||
|
datetime_zone_utc = UTC
|
||||||
|
datetime_zone_madrid = Madrid (Europe/Madrid)
|
||||||
|
datetime_zone_mexico = Mexico City (America/Mexico_City)
|
||||||
|
datetime_zone_tokyo = Tokyo (Asia/Tokyo)
|
||||||
|
|
||||||
|
datetime_block_formats = DateFormat / TimeFormat combinations
|
||||||
|
datetime_formats_date = DateFormat (date only)
|
||||||
|
datetime_formats_time = TimeFormat (time only)
|
||||||
|
datetime_formats_combos = format_datetime() combinations (date + time)
|
||||||
|
datetime_formats_iso = format_iso_datetime():
|
||||||
|
|
||||||
|
datetime_block_relative = RelativeFormat
|
||||||
|
datetime_relative_col_date = Date
|
||||||
|
|
||||||
|
datetime_block_since_until = DatePrecision (format_since / format_until)
|
||||||
|
datetime_since_until_col_precision = Precision
|
||||||
|
datetime_since_until_col_since = since
|
||||||
|
datetime_since_until_col_until = until
|
||||||
64
examples/locale/en-US/intro-flex.ftl
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
flex_slogan = Flexbox positioning
|
||||||
|
flex_block_title_direction = Direction
|
||||||
|
flex_block_title_justify = Justify content
|
||||||
|
flex_block_title_align = Align items
|
||||||
|
flex_block_title_align_self = Align self
|
||||||
|
flex_block_title_align_content = Align content
|
||||||
|
flex_block_title_grow_shrink = Grow and shrink
|
||||||
|
flex_block_title_other = Other examples
|
||||||
|
|
||||||
|
flex_title_direction_row = Row
|
||||||
|
flex_title_direction_row_reverse = Row reverse
|
||||||
|
flex_title_direction_column = Column
|
||||||
|
flex_title_direction_column_reverse = Column reverse
|
||||||
|
flex_title_justify_start = Justify content: start
|
||||||
|
flex_title_justify_center = Justify content: center
|
||||||
|
flex_title_justify_end = Justify content: end
|
||||||
|
flex_title_justify_between = Justify content: space between
|
||||||
|
flex_title_justify_around = Justify content: space around
|
||||||
|
flex_title_justify_evenly = Justify content: space evenly
|
||||||
|
flex_title_align_start = Align items: start
|
||||||
|
flex_title_align_center = Align items: center
|
||||||
|
flex_title_align_end = Align items: end
|
||||||
|
flex_title_align_stretch = Align items: stretch
|
||||||
|
flex_title_align_baseline = Align items: baseline
|
||||||
|
flex_title_align_self_start = Align self: start
|
||||||
|
flex_title_align_self_end = Align self: end
|
||||||
|
flex_title_align_self_center = Align self: center
|
||||||
|
flex_title_align_self_stretch = Align self: stretch
|
||||||
|
flex_title_align_self_baseline = Align self: baseline
|
||||||
|
flex_title_align_content_start = Align content: start
|
||||||
|
flex_title_align_content_end = Align content: end
|
||||||
|
flex_title_align_content_center = Align content: center
|
||||||
|
flex_title_align_content_between = Align content: space between
|
||||||
|
flex_title_align_content_around = Align content: space around
|
||||||
|
flex_title_align_content_evenly = Align content: space evenly
|
||||||
|
flex_title_align_content_stretch = Align content: stretch
|
||||||
|
flex_title_grow = Growth
|
||||||
|
flex_title_shrink = Shrink
|
||||||
|
flex_title_push_end = Automatic margin
|
||||||
|
flex_title_wrap = Line wrapping across multiple rows
|
||||||
|
flex_title_order = Visual order
|
||||||
|
flex_title_gap_none = Without spacing
|
||||||
|
flex_title_gap_some = With spacing
|
||||||
|
flex_title_grid_thirds = Column grid: three thirds
|
||||||
|
flex_title_grid_offset = Column grid: centered column with offset
|
||||||
|
flex_title_toolbar = Composite structure: toolbar
|
||||||
|
flex_desc_toolbar = FlexItem::push_end() on an intermediate item of the same Container with Flex
|
||||||
|
|
||||||
|
flex_item_label = Flex item { $n }
|
||||||
|
|
||||||
|
flex_box_tall = Tall
|
||||||
|
flex_box_medium = Medium
|
||||||
|
flex_box_short = Short
|
||||||
|
flex_box_fixed = Fixed
|
||||||
|
flex_box_grows = Grows to fill the space
|
||||||
|
flex_box_start_1 = Start 1
|
||||||
|
flex_box_start_2 = Start 2
|
||||||
|
flex_box_end = End
|
||||||
|
flex_box_half_centered = Half, centered
|
||||||
|
flex_box_file = File
|
||||||
|
flex_box_edit = Edit
|
||||||
|
flex_box_view = View
|
||||||
|
flex_box_profile = Profile
|
||||||
|
flex_box_logout = Sign out
|
||||||
49
examples/locale/en-US/intro-grid.ftl
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
grid_slogan = CSS Grid positioning
|
||||||
|
grid_block_title_columns = Columns
|
||||||
|
grid_block_title_rows_gap = Rows and gap
|
||||||
|
grid_block_title_placement = Item placement
|
||||||
|
grid_block_title_alignment = Align items
|
||||||
|
grid_block_title_content_alignment = Justify content
|
||||||
|
grid_block_title_item_alignment = Individual alignment
|
||||||
|
grid_block_title_auto_flow = Auto-placement algorithm
|
||||||
|
grid_block_title_auto_tracks = Implicit tracks
|
||||||
|
grid_block_title_responsive = Responsive grid
|
||||||
|
grid_block_title_layout = Composite structure: page layout
|
||||||
|
|
||||||
|
grid_title_columns_thirds = Three equal columns
|
||||||
|
grid_title_columns_sidebar = Fixed sidebar + flexible content
|
||||||
|
grid_title_columns_min_max = Min-content, fraction, max-content
|
||||||
|
grid_title_rows = Fixed-height rows
|
||||||
|
grid_title_gap_distinct = Different row and column spacing
|
||||||
|
grid_title_placement = Column spanning and explicit ranges
|
||||||
|
grid_title_alignment_start = Justify/align items: start
|
||||||
|
grid_title_alignment_center = Justify/align items: center
|
||||||
|
grid_title_alignment_end = Justify/align items: end
|
||||||
|
grid_title_alignment_stretch = Justify/align items: stretch
|
||||||
|
grid_title_content_alignment_start = Justify content: start
|
||||||
|
grid_title_content_alignment_center = Justify content: center
|
||||||
|
grid_title_content_alignment_between = Justify content: space between
|
||||||
|
grid_title_content_alignment_evenly = Justify content: space evenly
|
||||||
|
grid_title_item_alignment = Overriding the container's alignment on a single item
|
||||||
|
grid_title_auto_flow_row = Default flow, leaves a gap
|
||||||
|
grid_title_auto_flow_dense = Dense flow, fills the gap
|
||||||
|
grid_title_auto_tracks = Equal-height implicit rows
|
||||||
|
grid_title_responsive = One column on mobile, two on tablet, three on desktop
|
||||||
|
grid_title_layout = Header and footer spanning three columns, flexible content area
|
||||||
|
grid_desc_layout = Grid::new() with fixed side columns, a flexible center column and explicit spans
|
||||||
|
|
||||||
|
grid_box_label = Box { $n }
|
||||||
|
grid_box_card = Card { $n }
|
||||||
|
grid_box_sidebar = Sidebar
|
||||||
|
grid_box_content = Content
|
||||||
|
grid_box_min_content = Min-content
|
||||||
|
grid_box_fraction = 1fr
|
||||||
|
grid_box_max_content = Max-content
|
||||||
|
grid_box_plain = Plain
|
||||||
|
grid_box_span_2 = Spans 2 columns
|
||||||
|
grid_box_range = Columns 2 to 3
|
||||||
|
grid_box_overridden = Overridden
|
||||||
|
grid_box_header = Header
|
||||||
|
grid_box_nav = Navigation
|
||||||
|
grid_box_aside = Aside
|
||||||
|
grid_box_footer = Footer
|
||||||
36
examples/locale/en-US/intro-responsive.ftl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
responsive_slogan = Responsive behavior with Flex
|
||||||
|
responsive_note = Resize your browser window to see how the boxes below react. The breakpoints used are the usual Bootstrap ones: sm from 576px, md from 768px, lg from 992px.
|
||||||
|
|
||||||
|
responsive_block_title_activation = Flex activation at a breakpoint
|
||||||
|
responsive_block_title_direction = Direction change
|
||||||
|
responsive_block_title_grid = Column grid
|
||||||
|
responsive_block_title_justify_align = Justify and align
|
||||||
|
responsive_block_title_order = Visual order
|
||||||
|
responsive_block_title_gap_grow = Spacing and growth
|
||||||
|
|
||||||
|
responsive_title_activation = Stacked menu on mobile, in a row from md
|
||||||
|
responsive_title_direction = Image and text stacked on mobile; in a row, image on the right, from md
|
||||||
|
responsive_title_grid = From one column on mobile to three on desktop
|
||||||
|
responsive_title_justify_align = Centered header on mobile, spread out from md
|
||||||
|
responsive_title_order = Sidebar after the content on mobile, before it from lg
|
||||||
|
responsive_title_gap_grow = Growing spacing and a search box that only grows from md
|
||||||
|
|
||||||
|
responsive_box_nav_home = Home
|
||||||
|
responsive_box_nav_products = Products
|
||||||
|
responsive_box_nav_about = About
|
||||||
|
responsive_box_nav_contact = Contact
|
||||||
|
|
||||||
|
responsive_box_image = Image
|
||||||
|
responsive_box_text = Text
|
||||||
|
|
||||||
|
responsive_box_card = Card { $n }
|
||||||
|
|
||||||
|
responsive_box_logo = Logo
|
||||||
|
responsive_box_menu = Menu
|
||||||
|
|
||||||
|
responsive_box_content = Content
|
||||||
|
responsive_box_sidebar = Sidebar
|
||||||
|
|
||||||
|
responsive_box_file = File
|
||||||
|
responsive_box_edit = Edit
|
||||||
|
responsive_box_search = Search
|
||||||
19
examples/locale/en-US/intro-spacing.ftl
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
spacing_slogan = Native margin and padding with Margin and Padding
|
||||||
|
spacing_note = Margin and Padding have nothing to do with Flexbox: they apply to any component, whether its container uses Flexbox or not. They are combined with Flex here only to lay out the sample boxes in a row.
|
||||||
|
|
||||||
|
spacing_block_title_padding = Padding
|
||||||
|
spacing_block_title_layout = Layout
|
||||||
|
spacing_block_title_responsive = Padding at a breakpoint
|
||||||
|
spacing_block_title_combined = Margin and padding combined
|
||||||
|
|
||||||
|
spacing_title_uniform_small = Small padding on all four sides
|
||||||
|
spacing_title_uniform_medium = Medium padding on all four sides
|
||||||
|
spacing_title_uniform_large = Large padding on all four sides
|
||||||
|
spacing_title_sides = A different value per side, including the logical start/end sides
|
||||||
|
spacing_title_margin_gap = The middle box's margin pushes its neighbours away, without using Gap
|
||||||
|
spacing_title_margin_center = An automatic margin on both sides centers the box in the free space
|
||||||
|
spacing_title_responsive = Padding grows from md onwards, and again from lg
|
||||||
|
spacing_title_combined = Outer margin above/below and inner padding on all four sides, on a component with real content
|
||||||
|
|
||||||
|
spacing_box_sample = Content
|
||||||
|
spacing_box_card_button = Accept
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
|
menus_slogan = Navbars and menus
|
||||||
|
menus_note = Narrow the browser window to see how the navbars with a toggle button collapse.
|
||||||
|
|
||||||
|
## Blocks
|
||||||
|
|
||||||
|
menus_block_title_menus = Menus and dropdowns
|
||||||
|
menus_block_title_brand = Brand
|
||||||
|
menus_block_title_content = Other content
|
||||||
|
menus_block_title_colors = Color schemes
|
||||||
|
menus_block_title_container = Containers
|
||||||
|
menus_block_title_position = Placement
|
||||||
|
menus_block_title_layout = Layout and toggle button
|
||||||
|
menus_block_title_expand = Expand breakpoint
|
||||||
|
menus_block_title_offcanvas = Content in a side panel (Offcanvas)
|
||||||
|
|
||||||
|
## Menus and dropdowns
|
||||||
|
|
||||||
|
menus_title_full = Menu with dropdown, headers, dividers and a second menu at the end
|
||||||
|
|
||||||
|
menus_item_home = Home
|
||||||
menus_item_label = Label
|
menus_item_label = Label
|
||||||
menus_item_link = Link
|
menus_item_link = Link
|
||||||
menus_item_blank = External link
|
menus_item_blank = External link
|
||||||
menus_item_disabled = Disabled link
|
menus_item_disabled = Disabled link
|
||||||
|
menus_item_action = Action
|
||||||
|
menus_item_another = Another action
|
||||||
|
menus_item_something = Something else
|
||||||
|
|
||||||
menus_test_title = Dropdown
|
menus_test_title = Dropdown
|
||||||
|
|
||||||
|
|
@ -21,3 +44,66 @@ menus_plugin_cache = Rust Plugin Cache
|
||||||
|
|
||||||
menus_item_sign_up = Sign up
|
menus_item_sign_up = Sign up
|
||||||
menus_item_login = Login
|
menus_item_login = Login
|
||||||
|
|
||||||
|
## Brand
|
||||||
|
|
||||||
|
menus_title_brand_link = Text only, as a link
|
||||||
|
menus_title_brand_span = Text only, without a link
|
||||||
|
menus_title_brand_image = Image only
|
||||||
|
menus_title_brand_both = Image and text
|
||||||
|
|
||||||
|
## Other content
|
||||||
|
|
||||||
|
menus_title_form_search = Search form next to the brand
|
||||||
|
menus_title_form_buttons = Buttons
|
||||||
|
menus_title_form_group = Input group
|
||||||
|
menus_title_text = Text
|
||||||
|
menus_title_brand_and_text = Brand and text
|
||||||
|
menus_title_nav_text = Menu and text
|
||||||
|
|
||||||
|
menus_search_label = Search
|
||||||
|
menus_search_button = Search
|
||||||
|
menus_button_main = Main button
|
||||||
|
menus_button_small = Smaller button
|
||||||
|
menus_group_placeholder = Username
|
||||||
|
menus_text_inline = Navbar text
|
||||||
|
|
||||||
|
## Color schemes
|
||||||
|
|
||||||
|
menus_title_color_dark = Dark background
|
||||||
|
menus_title_color_primary = Primary color with light text
|
||||||
|
menus_title_color_light = Light theme background
|
||||||
|
menus_title_color_custom = Custom color
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
menus_title_container_full = Takes all the available width
|
||||||
|
menus_title_container_max = Inside a container with a maximum width
|
||||||
|
|
||||||
|
## Placement
|
||||||
|
|
||||||
|
menus_position_note = Navbars are anchored to the frame of each example, not to the window, so they don't cover the rest of the page. Scroll the frames with sticky navbars.
|
||||||
|
menus_title_position_fixed_top = Fixed top
|
||||||
|
menus_title_position_fixed_bottom = Fixed bottom
|
||||||
|
menus_title_position_sticky_top = Sticky top
|
||||||
|
menus_title_position_sticky_bottom = Sticky bottom
|
||||||
|
menus_filler = Page content. Fixed elements are taken out of the document flow, while sticky ones stay at the edge of the scrolling container.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
menus_title_layout_toggle = Toggle button only, no brand
|
||||||
|
menus_title_layout_brand_left = Brand on the left and toggle button on the right
|
||||||
|
menus_title_layout_brand_right = Toggle button on the left and brand on the right
|
||||||
|
menus_title_layout_brand_always = Always visible brand, no toggle button
|
||||||
|
|
||||||
|
## Expand breakpoint
|
||||||
|
|
||||||
|
menus_title_expand = Expands from { $name } ({ $width })
|
||||||
|
|
||||||
|
## Offcanvas
|
||||||
|
|
||||||
|
menus_offcanvas_title = Main menu
|
||||||
|
menus_title_offcanvas_start = Panel from the start, no brand
|
||||||
|
menus_title_offcanvas_brand_left = Brand on the left and panel from the end
|
||||||
|
menus_title_offcanvas_brand_right = Brand on the right and panel without backdrop
|
||||||
|
menus_title_offcanvas_dark = Dark navbar and panel, with a static backdrop
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -2,7 +2,9 @@ title = Controles de formulario
|
||||||
slogan = Componentes Bootsier para formularios
|
slogan = Componentes Bootsier para formularios
|
||||||
block_selections = Casillas, interruptores y botones de opción
|
block_selections = Casillas, interruptores y botones de opción
|
||||||
block_text = Campos de texto, multilínea y rango
|
block_text = Campos de texto, multilínea y rango
|
||||||
block_lists = Listas de selección y etiquetas flotantes
|
block_lists = Listas de selección
|
||||||
|
block_lists_floating = Listas de selección y etiquetas flotantes
|
||||||
|
block_dialog = Diálogo modal
|
||||||
|
|
||||||
fieldset_text = Campos de texto
|
fieldset_text = Campos de texto
|
||||||
label_name = Nombre completo
|
label_name = Nombre completo
|
||||||
|
|
@ -16,6 +18,8 @@ label_url = Sitio web
|
||||||
placeholder_url = https://ejemplo.com
|
placeholder_url = https://ejemplo.com
|
||||||
label_search = Búsqueda
|
label_search = Búsqueda
|
||||||
placeholder_search = Término de búsqueda...
|
placeholder_search = Término de búsqueda...
|
||||||
|
help_name = Introduce tu nombre completo tal como aparece en tu documento de identidad.
|
||||||
|
help_email = Solo usaremos tu correo para enviarte notificaciones importantes.
|
||||||
|
|
||||||
fieldset_textarea = Texto multilínea
|
fieldset_textarea = Texto multilínea
|
||||||
label_comment = Comentario
|
label_comment = Comentario
|
||||||
|
|
@ -72,3 +76,8 @@ help_rating = De 1 (muy malo) a 10 (excelente).
|
||||||
btn_submit = Enviar
|
btn_submit = Enviar
|
||||||
btn_reset = Restablecer
|
btn_reset = Restablecer
|
||||||
btn_cancel = Cancelar
|
btn_cancel = Cancelar
|
||||||
|
btn_ok = Aceptar
|
||||||
|
btn_delete = Eliminar registro
|
||||||
|
|
||||||
|
dialog_delete_title = Eliminar registro
|
||||||
|
dialog_delete_body = ¿Seguro que quieres eliminar este registro? Esta acción no se puede deshacer.
|
||||||
|
|
|
||||||
21
examples/locale/es-ES/intro-datetime.ftl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
datetime_slogan = Zonas horarias y formatos de fecha y hora
|
||||||
|
|
||||||
|
datetime_block_world = El mismo instante, alrededor del mundo
|
||||||
|
datetime_zone_utc = UTC
|
||||||
|
datetime_zone_madrid = Madrid (Europe/Madrid)
|
||||||
|
datetime_zone_mexico = Ciudad de México (America/Mexico_City)
|
||||||
|
datetime_zone_tokyo = Tokio (Asia/Tokyo)
|
||||||
|
|
||||||
|
datetime_block_formats = Combinaciones de DateFormat / TimeFormat
|
||||||
|
datetime_formats_date = DateFormat (sólo fecha)
|
||||||
|
datetime_formats_time = TimeFormat (sólo hora)
|
||||||
|
datetime_formats_combos = Combinaciones de format_datetime() (fecha + hora)
|
||||||
|
datetime_formats_iso = format_iso_datetime():
|
||||||
|
|
||||||
|
datetime_block_relative = RelativeFormat
|
||||||
|
datetime_relative_col_date = Fecha
|
||||||
|
|
||||||
|
datetime_block_since_until = DatePrecision (format_since / format_until)
|
||||||
|
datetime_since_until_col_precision = Precisión
|
||||||
|
datetime_since_until_col_since = desde
|
||||||
|
datetime_since_until_col_until = hasta
|
||||||
64
examples/locale/es-ES/intro-flex.ftl
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
flex_slogan = Posicionamiento con Flexbox
|
||||||
|
flex_block_title_direction = Dirección
|
||||||
|
flex_block_title_justify = Justificación de contenido
|
||||||
|
flex_block_title_align = Alineación de elementos
|
||||||
|
flex_block_title_align_self = Alineación individual
|
||||||
|
flex_block_title_align_content = Alineación de contenido
|
||||||
|
flex_block_title_grow_shrink = Crecimiento y reducción
|
||||||
|
flex_block_title_other = Otros ejemplos
|
||||||
|
|
||||||
|
flex_title_direction_row = Fila
|
||||||
|
flex_title_direction_row_reverse = Fila invertida
|
||||||
|
flex_title_direction_column = Columna
|
||||||
|
flex_title_direction_column_reverse = Columna invertida
|
||||||
|
flex_title_justify_start = Justificación: inicio
|
||||||
|
flex_title_justify_center = Justificación: centro
|
||||||
|
flex_title_justify_end = Justificación: final
|
||||||
|
flex_title_justify_between = Justificación: espacio entre elementos
|
||||||
|
flex_title_justify_around = Justificación: espacio alrededor de cada elemento
|
||||||
|
flex_title_justify_evenly = Justificación: espacio repartido a partes iguales
|
||||||
|
flex_title_align_start = Alineación: inicio
|
||||||
|
flex_title_align_center = Alineación: centro
|
||||||
|
flex_title_align_end = Alineación: final
|
||||||
|
flex_title_align_stretch = Alineación: estirar
|
||||||
|
flex_title_align_baseline = Alineación: línea base
|
||||||
|
flex_title_align_self_start = Alineación individual: inicio
|
||||||
|
flex_title_align_self_end = Alineación individual: final
|
||||||
|
flex_title_align_self_center = Alineación individual: centro
|
||||||
|
flex_title_align_self_stretch = Alineación individual: estirar
|
||||||
|
flex_title_align_self_baseline = Alineación individual: línea base
|
||||||
|
flex_title_align_content_start = Alineación de contenido: inicio
|
||||||
|
flex_title_align_content_end = Alineación de contenido: final
|
||||||
|
flex_title_align_content_center = Alineación de contenido: centro
|
||||||
|
flex_title_align_content_between = Alineación de contenido: espacio entre líneas
|
||||||
|
flex_title_align_content_around = Alineación de contenido: espacio alrededor de cada línea
|
||||||
|
flex_title_align_content_evenly = Alineación de contenido: espacio repartido a partes iguales
|
||||||
|
flex_title_align_content_stretch = Alineación de contenido: estirar
|
||||||
|
flex_title_grow = Crecimiento
|
||||||
|
flex_title_shrink = Reducción
|
||||||
|
flex_title_push_end = Margen automático
|
||||||
|
flex_title_wrap = Ajuste de línea con múltiples filas
|
||||||
|
flex_title_order = Orden visual
|
||||||
|
flex_title_gap_none = Sin espaciado
|
||||||
|
flex_title_gap_some = Con espaciado
|
||||||
|
flex_title_grid_thirds = Rejilla de columnas: tres tercios
|
||||||
|
flex_title_grid_offset = Rejilla de columnas: columna centrada con desplazamiento
|
||||||
|
flex_title_toolbar = Estructura compuesta: barra de herramientas
|
||||||
|
flex_desc_toolbar = FlexItem::push_end() sobre un elemento intermedio del propio Container con Flex
|
||||||
|
|
||||||
|
flex_item_label = Flex ítem { $n }
|
||||||
|
|
||||||
|
flex_box_tall = Alto
|
||||||
|
flex_box_medium = Medio
|
||||||
|
flex_box_short = Bajo
|
||||||
|
flex_box_fixed = Fijo
|
||||||
|
flex_box_grows = Crece para llenar el espacio
|
||||||
|
flex_box_start_1 = Inicio 1
|
||||||
|
flex_box_start_2 = Inicio 2
|
||||||
|
flex_box_end = Final
|
||||||
|
flex_box_half_centered = Mitad, centrada
|
||||||
|
flex_box_file = Archivo
|
||||||
|
flex_box_edit = Editar
|
||||||
|
flex_box_view = Ver
|
||||||
|
flex_box_profile = Perfil
|
||||||
|
flex_box_logout = Salir
|
||||||
49
examples/locale/es-ES/intro-grid.ftl
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
grid_slogan = Posicionamiento con CSS Grid
|
||||||
|
grid_block_title_columns = Columnas
|
||||||
|
grid_block_title_rows_gap = Filas y espaciado
|
||||||
|
grid_block_title_placement = Colocación de elementos
|
||||||
|
grid_block_title_alignment = Alineación de elementos
|
||||||
|
grid_block_title_content_alignment = Justificación de contenido
|
||||||
|
grid_block_title_item_alignment = Alineación individual
|
||||||
|
grid_block_title_auto_flow = Algoritmo de colocación automática
|
||||||
|
grid_block_title_auto_tracks = Pistas implícitas
|
||||||
|
grid_block_title_responsive = Rejilla responsive
|
||||||
|
grid_block_title_layout = Estructura compuesta: maquetación de página
|
||||||
|
|
||||||
|
grid_title_columns_thirds = Tres columnas iguales
|
||||||
|
grid_title_columns_sidebar = Barra lateral fija + contenido flexible
|
||||||
|
grid_title_columns_min_max = Contenido mínimo, fracción, contenido máximo
|
||||||
|
grid_title_rows = Filas de altura fija
|
||||||
|
grid_title_gap_distinct = Espaciado distinto entre filas y columnas
|
||||||
|
grid_title_placement = Columnas abarcadas y rangos explícitos
|
||||||
|
grid_title_alignment_start = Justificación/alineación de elementos: inicio
|
||||||
|
grid_title_alignment_center = Justificación/alineación de elementos: centro
|
||||||
|
grid_title_alignment_end = Justificación/alineación de elementos: final
|
||||||
|
grid_title_alignment_stretch = Justificación/alineación de elementos: estirar
|
||||||
|
grid_title_content_alignment_start = Justificación de contenido: inicio
|
||||||
|
grid_title_content_alignment_center = Justificación de contenido: centro
|
||||||
|
grid_title_content_alignment_between = Justificación de contenido: espacio entre elementos
|
||||||
|
grid_title_content_alignment_evenly = Justificación de contenido: espacio repartido a partes iguales
|
||||||
|
grid_title_item_alignment = Sobrescribir la alineación del contenedor en un único elemento
|
||||||
|
grid_title_auto_flow_row = Flujo por defecto, deja un hueco
|
||||||
|
grid_title_auto_flow_dense = Flujo denso, rellena el hueco
|
||||||
|
grid_title_auto_tracks = Filas implícitas de igual altura
|
||||||
|
grid_title_responsive = Una columna en móvil, dos en tableta, tres en escritorio
|
||||||
|
grid_title_layout = Cabecera y pie abarcando tres columnas, área de contenido flexible
|
||||||
|
grid_desc_layout = Grid::new() con columnas laterales fijas, una columna central flexible y abarcados explícitos
|
||||||
|
|
||||||
|
grid_box_label = Caja { $n }
|
||||||
|
grid_box_card = Tarjeta { $n }
|
||||||
|
grid_box_sidebar = Barra lateral
|
||||||
|
grid_box_content = Contenido
|
||||||
|
grid_box_min_content = Contenido mínimo
|
||||||
|
grid_box_fraction = 1fr
|
||||||
|
grid_box_max_content = Contenido máximo
|
||||||
|
grid_box_plain = Normal
|
||||||
|
grid_box_span_2 = Abarca 2 columnas
|
||||||
|
grid_box_range = Columnas 2 a 3
|
||||||
|
grid_box_overridden = Sobrescrita
|
||||||
|
grid_box_header = Cabecera
|
||||||
|
grid_box_nav = Navegación
|
||||||
|
grid_box_aside = Aparte
|
||||||
|
grid_box_footer = Pie
|
||||||
36
examples/locale/es-ES/intro-responsive.ftl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
responsive_slogan = Comportamiento responsive con Flex
|
||||||
|
responsive_note = Cambia el ancho de la ventana del navegador para ver cómo reaccionan las siguientes cajas. Los puntos de corte usados son los habituales de Bootstrap: sm a partir de 576px, md a partir de 768px, lg a partir de 992px.
|
||||||
|
|
||||||
|
responsive_block_title_activation = Activación de Flex por punto de corte
|
||||||
|
responsive_block_title_direction = Cambio de dirección
|
||||||
|
responsive_block_title_grid = Rejilla de columnas
|
||||||
|
responsive_block_title_justify_align = Justificación y alineación
|
||||||
|
responsive_block_title_order = Orden visual
|
||||||
|
responsive_block_title_gap_grow = Espaciado y crecimiento
|
||||||
|
|
||||||
|
responsive_title_activation = Menú apilado en móvil, en fila a partir de md
|
||||||
|
responsive_title_direction = Imagen y texto apilados en móvil; en fila, con la imagen a la derecha, a partir de md
|
||||||
|
responsive_title_grid = De una columna en móvil a tres en escritorio
|
||||||
|
responsive_title_justify_align = Cabecera centrada en móvil, distribuida a partir de md
|
||||||
|
responsive_title_order = Barra lateral después del contenido en móvil, antes a partir de lg
|
||||||
|
responsive_title_gap_grow = Espaciado creciente y buscador que sólo crece a partir de md
|
||||||
|
|
||||||
|
responsive_box_nav_home = Inicio
|
||||||
|
responsive_box_nav_products = Productos
|
||||||
|
responsive_box_nav_about = Nosotros
|
||||||
|
responsive_box_nav_contact = Contacto
|
||||||
|
|
||||||
|
responsive_box_image = Imagen
|
||||||
|
responsive_box_text = Texto
|
||||||
|
|
||||||
|
responsive_box_card = Tarjeta { $n }
|
||||||
|
|
||||||
|
responsive_box_logo = Logotipo
|
||||||
|
responsive_box_menu = Menú
|
||||||
|
|
||||||
|
responsive_box_content = Contenido
|
||||||
|
responsive_box_sidebar = Barra lateral
|
||||||
|
|
||||||
|
responsive_box_file = Archivo
|
||||||
|
responsive_box_edit = Editar
|
||||||
|
responsive_box_search = Buscador
|
||||||
19
examples/locale/es-ES/intro-spacing.ftl
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
spacing_slogan = Márgenes y relleno nativos con Margin y Padding
|
||||||
|
spacing_note = Margin y Padding no tienen relación con Flexbox: se aplican sobre cualquier componente, use o no Flexbox su contenedor. Aquí se combinan con Flex sólo para colocar las cajas de muestra en fila.
|
||||||
|
|
||||||
|
spacing_block_title_padding = Rellenos
|
||||||
|
spacing_block_title_layout = Composición
|
||||||
|
spacing_block_title_responsive = Relleno por punto de corte
|
||||||
|
spacing_block_title_combined = Margin y padding combinados
|
||||||
|
|
||||||
|
spacing_title_uniform_small = Relleno pequeño en los cuatro lados
|
||||||
|
spacing_title_uniform_medium = Relleno medio en los cuatro lados
|
||||||
|
spacing_title_uniform_large = Relleno grande en los cuatro lados
|
||||||
|
spacing_title_sides = Un valor distinto para cada lado, incluidos los lados lógicos de inicio y fin
|
||||||
|
spacing_title_margin_gap = El margen de la caja central empuja a sus vecinas, sin usar Gap
|
||||||
|
spacing_title_margin_center = Un margen automático en ambos lados centra la caja en el espacio libre
|
||||||
|
spacing_title_responsive = El relleno crece a partir de md, y de nuevo a partir de lg
|
||||||
|
spacing_title_combined = Margen exterior arriba/abajo y relleno interno por los cuatro lados, sobre un componente con contenido real
|
||||||
|
|
||||||
|
spacing_box_sample = Contenido
|
||||||
|
spacing_box_card_button = Aceptar
|
||||||
|
|
@ -1,7 +1,30 @@
|
||||||
|
menus_slogan = Barras de navegación y menús
|
||||||
|
menus_note = Reduce el ancho de la ventana para ver cómo colapsan las barras que tienen botón de despliegue.
|
||||||
|
|
||||||
|
## Bloques
|
||||||
|
|
||||||
|
menus_block_title_menus = Menús y desplegables
|
||||||
|
menus_block_title_brand = Marca de identidad
|
||||||
|
menus_block_title_content = Otros contenidos
|
||||||
|
menus_block_title_colors = Esquemas de color
|
||||||
|
menus_block_title_container = Contenedores
|
||||||
|
menus_block_title_position = Posición
|
||||||
|
menus_block_title_layout = Disposición y botón de despliegue
|
||||||
|
menus_block_title_expand = Punto de corte de expansión
|
||||||
|
menus_block_title_offcanvas = Contenido en un panel lateral (Offcanvas)
|
||||||
|
|
||||||
|
## Menús y desplegables
|
||||||
|
|
||||||
|
menus_title_full = Menú con desplegable, cabeceras, separadores y un segundo menú al final
|
||||||
|
|
||||||
|
menus_item_home = Inicio
|
||||||
menus_item_label = Etiqueta
|
menus_item_label = Etiqueta
|
||||||
menus_item_link = Enlace
|
menus_item_link = Enlace
|
||||||
menus_item_blank = Enlace externo
|
menus_item_blank = Externo
|
||||||
menus_item_disabled = Enlace deshabilitado
|
menus_item_disabled = Deshabilitado
|
||||||
|
menus_item_action = Acción
|
||||||
|
menus_item_another = Otra acción
|
||||||
|
menus_item_something = Algo más
|
||||||
|
|
||||||
menus_test_title = Desplegable
|
menus_test_title = Desplegable
|
||||||
|
|
||||||
|
|
@ -21,3 +44,66 @@ menus_plugin_cache = Plugin Rust de caché
|
||||||
|
|
||||||
menus_item_sign_up = Registrarse
|
menus_item_sign_up = Registrarse
|
||||||
menus_item_login = Iniciar sesión
|
menus_item_login = Iniciar sesión
|
||||||
|
|
||||||
|
## Marca de identidad
|
||||||
|
|
||||||
|
menus_title_brand_link = Sólo texto, como enlace
|
||||||
|
menus_title_brand_span = Sólo texto, sin enlace
|
||||||
|
menus_title_brand_image = Sólo imagen
|
||||||
|
menus_title_brand_both = Imagen y texto
|
||||||
|
|
||||||
|
## Otros contenidos
|
||||||
|
|
||||||
|
menus_title_form_search = Formulario de búsqueda junto a la marca
|
||||||
|
menus_title_form_buttons = Botones
|
||||||
|
menus_title_form_group = Grupo de campos
|
||||||
|
menus_title_text = Texto
|
||||||
|
menus_title_brand_and_text = Marca y texto
|
||||||
|
menus_title_nav_text = Menú y texto
|
||||||
|
|
||||||
|
menus_search_label = Buscar
|
||||||
|
menus_search_button = Buscar
|
||||||
|
menus_button_main = Botón principal
|
||||||
|
menus_button_small = Botón pequeño
|
||||||
|
menus_group_placeholder = Usuario
|
||||||
|
menus_text_inline = Texto de la barra de navegación
|
||||||
|
|
||||||
|
## Esquemas de color
|
||||||
|
|
||||||
|
menus_title_color_dark = Fondo oscuro
|
||||||
|
menus_title_color_primary = Color primario con texto claro
|
||||||
|
menus_title_color_light = Fondo claro del tema
|
||||||
|
menus_title_color_custom = Color personalizado
|
||||||
|
|
||||||
|
## Contenedores
|
||||||
|
|
||||||
|
menus_title_container_full = Ocupa todo el ancho disponible
|
||||||
|
menus_title_container_max = Dentro de un contenedor de ancho máximo
|
||||||
|
|
||||||
|
## Posición
|
||||||
|
|
||||||
|
menus_position_note = Las barras se anclan al marco de cada ejemplo, no a la ventana, para no tapar el resto de la página. Desplaza el contenido de los marcos con barras pegajosas.
|
||||||
|
menus_title_position_fixed_top = Fija arriba
|
||||||
|
menus_title_position_fixed_bottom = Fija abajo
|
||||||
|
menus_title_position_sticky_top = Pegajosa arriba
|
||||||
|
menus_title_position_sticky_bottom = Pegajosa abajo
|
||||||
|
menus_filler = Contenido de la página. Los elementos con posición fija salen del flujo del documento, y los pegajosos se quedan en el borde del contenedor que se desplaza.
|
||||||
|
|
||||||
|
## Disposición
|
||||||
|
|
||||||
|
menus_title_layout_toggle = Sólo botón de despliegue, sin marca
|
||||||
|
menus_title_layout_brand_left = Marca a la izquierda y botón de despliegue a la derecha
|
||||||
|
menus_title_layout_brand_right = Botón de despliegue a la izquierda y marca a la derecha
|
||||||
|
menus_title_layout_brand_always = Marca siempre visible, sin botón de despliegue
|
||||||
|
|
||||||
|
## Punto de corte
|
||||||
|
|
||||||
|
menus_title_expand = Se expande a partir de { $name } ({ $width })
|
||||||
|
|
||||||
|
## Offcanvas
|
||||||
|
|
||||||
|
menus_offcanvas_title = Menú principal
|
||||||
|
menus_title_offcanvas_start = Panel desde el inicio, sin marca
|
||||||
|
menus_title_offcanvas_brand_left = Marca a la izquierda y panel desde el final
|
||||||
|
menus_title_offcanvas_brand_right = Marca a la derecha y panel sin capa de fondo
|
||||||
|
menus_title_offcanvas_dark = Barra y panel oscuros, con capa de fondo estática
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,783 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use pagetop_bootsier::theme::*;
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
include_locales!(LOC from "examples/locale");
|
include_locales!(LOC from "examples/locale");
|
||||||
|
|
||||||
struct SuperMenu;
|
struct NavbarMenus;
|
||||||
|
|
||||||
impl Extension for SuperMenu {
|
#[async_trait]
|
||||||
|
impl Extension for NavbarMenus {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![&pagetop_bootsier::Bootsier]
|
||||||
&pagetop_aliner::Aliner,
|
|
||||||
&pagetop_bootsier::Bootsier,
|
|
||||||
&pagetop::base::extension::Welcome,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(&self) {
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
router.route("/", web::get(navbar_menus))
|
||||||
.with_expand(BreakPoint::LG)
|
}
|
||||||
.with_item(navbar::Item::nav(
|
}
|
||||||
Nav::new()
|
|
||||||
.with_item(nav::Item::link(L10n::t("menus_item_link", &LOC), |cx| {
|
|
||||||
cx.route("/")
|
|
||||||
}))
|
|
||||||
.with_item(nav::Item::link_blank(
|
|
||||||
L10n::t("menus_item_blank", &LOC),
|
|
||||||
|_| "https://docs.rs/pagetop".into(),
|
|
||||||
))
|
|
||||||
.with_item(nav::Item::dropdown(
|
|
||||||
Dropdown::new()
|
|
||||||
.with_title(L10n::t("menus_test_title", &LOC))
|
|
||||||
.with_item(dropdown::Item::header(L10n::t("menus_dev_header", &LOC)))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_dev_getting_started", &LOC),
|
|
||||||
|cx| cx.route("/dev/getting-started"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_dev_guides", &LOC),
|
|
||||||
|cx| cx.route("/dev/guides"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::link_blank(
|
|
||||||
L10n::t("menus_dev_forum", &LOC),
|
|
||||||
|_| "https://forum.example.dev".into(),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::divider())
|
|
||||||
.with_item(dropdown::Item::header(L10n::t("menus_sdk_header", &LOC)))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_sdk_rust", &LOC),
|
|
||||||
|cx| cx.route("/dev/sdks/rust"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::link(L10n::t("menus_sdk_js", &LOC), |cx| {
|
|
||||||
cx.route("/dev/sdks/js")
|
|
||||||
}))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_sdk_python", &LOC),
|
|
||||||
|cx| cx.route("/dev/sdks/python"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::divider())
|
|
||||||
.with_item(dropdown::Item::header(L10n::t("menus_plugin_header", &LOC)))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_plugin_auth", &LOC),
|
|
||||||
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::link(
|
|
||||||
L10n::t("menus_plugin_cache", &LOC),
|
|
||||||
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
|
||||||
))
|
|
||||||
.with_item(dropdown::Item::divider())
|
|
||||||
.with_item(dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
|
|
||||||
.with_item(dropdown::Item::link_disabled(
|
|
||||||
L10n::t("menus_item_disabled", &LOC),
|
|
||||||
|cx| cx.route("#"),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.with_item(nav::Item::link_disabled(
|
|
||||||
L10n::t("menus_item_disabled", &LOC),
|
|
||||||
|cx| cx.route("#"),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
.with_item(navbar::Item::nav(
|
|
||||||
Nav::new()
|
|
||||||
.with_classes(
|
|
||||||
ClassesOp::Add,
|
|
||||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
|
||||||
)
|
|
||||||
.with_item(nav::Item::link(L10n::t("menus_item_sign_up", &LOC), |cx| {
|
|
||||||
cx.route("/auth/sign-up")
|
|
||||||
}))
|
|
||||||
.with_item(nav::Item::link(L10n::t("menus_item_login", &LOC), |cx| {
|
|
||||||
cx.route("/auth/login")
|
|
||||||
})),
|
|
||||||
));
|
|
||||||
|
|
||||||
InRegion::Global(&DefaultRegion::Header).add(
|
async fn navbar_menus(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Container::new()
|
let page = Page::new(request)
|
||||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
.with_assets(demo_title_styles())
|
||||||
.with_child(navbar_menu),
|
.with_assets(demo_code_styles())
|
||||||
|
.with_assets(demo_frame_styles())
|
||||||
|
.with_assets(demo_scroll_styles());
|
||||||
|
|
||||||
|
let mut intro = Intro::custom()
|
||||||
|
.with_width(intro::Width::Full)
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_slogan(Lc::t("menus_slogan", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p class="intro-text-lead" { (Lc::t("menus_note", &LOC).using(cx)) }
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.with_child(menus_block())
|
||||||
|
.with_child(brand_block())
|
||||||
|
.with_child(container_block())
|
||||||
|
.with_child(layout_block())
|
||||||
|
.with_child(expand_block())
|
||||||
|
.with_child(content_block())
|
||||||
|
.with_child(position_block());
|
||||||
|
|
||||||
|
// Demostraciones asociadas a Bootsier: usan sus tipos, o clases y variables CSS de Bootstrap
|
||||||
|
// que otros temas no definen. Sólo se muestran con el tema Bootsier activo.
|
||||||
|
if is_bootsier() {
|
||||||
|
intro = intro
|
||||||
|
.with_child(colors_block())
|
||||||
|
.with_child(offcanvas_block());
|
||||||
|
}
|
||||||
|
|
||||||
|
page.with_child(intro).render().await
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Menús >**************************************************************************************
|
||||||
|
|
||||||
|
fn menus_block() -> Block {
|
||||||
|
let full_menu = Navbar::brand_left(demo_brand())
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(
|
||||||
|
Nav::new()
|
||||||
|
.with_item(nav::Item::link(Lc::t("menus_item_home", &LOC), "/").with_active(true))
|
||||||
|
.with_item(nav::Item::link_blank(
|
||||||
|
Lc::t("menus_item_blank", &LOC),
|
||||||
|
"https://docs.rs/pagetop",
|
||||||
|
))
|
||||||
|
.with_item(nav::Item::dropdown(
|
||||||
|
Dropdown::new()
|
||||||
|
.with_title(Lc::t("menus_test_title", &LOC))
|
||||||
|
.with_item(dropdown::Item::header(Lc::t("menus_dev_header", &LOC)))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_dev_getting_started", &LOC),
|
||||||
|
"/dev/getting-started",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_dev_guides", &LOC),
|
||||||
|
"/dev/guides",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link_blank(
|
||||||
|
Lc::t("menus_dev_forum", &LOC),
|
||||||
|
"https://forum.example.dev",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::divider())
|
||||||
|
.with_item(dropdown::Item::header(Lc::t("menus_sdk_header", &LOC)))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_sdk_rust", &LOC),
|
||||||
|
"/dev/sdks/rust",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_sdk_js", &LOC),
|
||||||
|
"/dev/sdks/js",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_sdk_python", &LOC),
|
||||||
|
"/dev/sdks/python",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::divider())
|
||||||
|
.with_item(dropdown::Item::header(Lc::t("menus_plugin_header", &LOC)))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_plugin_auth", &LOC),
|
||||||
|
"/dev/sdks/rust/plugins/auth",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_plugin_cache", &LOC),
|
||||||
|
"/dev/sdks/rust/plugins/cache",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::divider())
|
||||||
|
.with_item(dropdown::Item::label(Lc::t("menus_item_label", &LOC)))
|
||||||
|
.with_item(dropdown::Item::link_disabled(
|
||||||
|
Lc::t("menus_item_disabled", &LOC),
|
||||||
|
"#",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_item(nav::Item::link_disabled(
|
||||||
|
Lc::t("menus_item_disabled", &LOC),
|
||||||
|
"#",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_item(navbar::Item::nav(
|
||||||
|
Nav::new()
|
||||||
|
// Empuja este menú (y lo que le siga) al extremo final de la barra.
|
||||||
|
.with_prop(FlexItem::push_end())
|
||||||
|
.with_item(nav::Item::link(
|
||||||
|
Lc::t("menus_item_sign_up", &LOC),
|
||||||
|
"/auth/sign-up",
|
||||||
|
))
|
||||||
|
.with_item(nav::Item::link(
|
||||||
|
Lc::t("menus_item_login", &LOC),
|
||||||
|
"/auth/login",
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
|
||||||
|
add_demo(
|
||||||
|
Block::new().with_title(Lc::t("menus_block_title_menus", &LOC)),
|
||||||
|
Lc::t("menus_title_full", &LOC),
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
".with_item(navbar::Item::nav(Nav::new()...with_item(nav::Item::dropdown(...))))",
|
||||||
|
".with_item(navbar::Item::nav(Nav::new().with_prop(FlexItem::push_end())...))",
|
||||||
|
),
|
||||||
|
full_menu,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Marca de identidad >*************************************************************************
|
||||||
|
|
||||||
|
fn brand_block() -> Block {
|
||||||
|
// Una barra sin contenidos no se renderiza, así que la marca se añade como un elemento más.
|
||||||
|
let brand_variants: [(&str, Brand, &str); 4] = [
|
||||||
|
(
|
||||||
|
"menus_title_brand_link",
|
||||||
|
demo_brand(),
|
||||||
|
"navbar::Item::brand(Brand::new().with_title(...).with_route(Route::from(\"/\")))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_brand_span",
|
||||||
|
demo_brand().with_route(None::<Route>),
|
||||||
|
"navbar::Item::brand(Brand::new().with_title(...).with_route(None))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_brand_image",
|
||||||
|
Brand::new()
|
||||||
|
.with_title(Lc::none())
|
||||||
|
.with_image(demo_logo())
|
||||||
|
.with_route(Route::from("/")),
|
||||||
|
"navbar::Item::brand(Brand::new().with_title(Lc::none()).with_image(image))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_brand_both",
|
||||||
|
demo_brand().with_image(demo_logo()),
|
||||||
|
"navbar::Item::brand(Brand::new().with_title(...).with_image(image))",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_brand", &LOC));
|
||||||
|
for (title_key, brand, code) in brand_variants {
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t(title_key, &LOC),
|
||||||
|
code,
|
||||||
|
Navbar::simple()
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_item(navbar::Item::brand(brand)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Otros contenidos >***************************************************************************
|
||||||
|
|
||||||
|
fn content_block() -> Block {
|
||||||
|
let gap = align::Gap::Both(UnitValue::RelRem(0.5));
|
||||||
|
|
||||||
|
let search_form = Form::new()
|
||||||
|
.with_action("/")
|
||||||
|
.with_method(form::Method::Get)
|
||||||
|
.with_child(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Center)
|
||||||
|
.with_gap(gap)
|
||||||
|
.with_child(
|
||||||
|
form::input::Field::search()
|
||||||
|
.with_name("q")
|
||||||
|
.with_placeholder(Lc::t("menus_search_label", &LOC))
|
||||||
|
// Sin el margen inferior con que algunos temas separan los campos.
|
||||||
|
.with_prop(Margin::new().with_bottom(UnitValue::Zero)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::submit(Lc::t("menus_search_button", &LOC))
|
||||||
|
.with_style(button::Style::Outline(Intent::Success)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let buttons = Form::new().with_child(
|
||||||
|
Flex::new()
|
||||||
|
.with_align(align::Items::Center)
|
||||||
|
.with_gap(gap)
|
||||||
|
.with_child(
|
||||||
|
Button::plain(Lc::t("menus_button_main", &LOC))
|
||||||
|
.with_style(button::Style::Outline(Intent::Success)),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Button::plain(Lc::t("menus_button_small", &LOC))
|
||||||
|
.with_size(button::Size::Small)
|
||||||
|
.with_style(button::Style::Outline(Intent::Neutral)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut content_variants: Vec<(&str, Navbar, &str)> = vec![
|
||||||
|
(
|
||||||
|
"menus_title_form_search",
|
||||||
|
Navbar::simple_brand_left(demo_brand()).with_item(navbar::Item::form(search_form)),
|
||||||
|
"Navbar::simple_brand_left(brand).with_item(navbar::Item::form(Form::new()...))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_form_buttons",
|
||||||
|
Navbar::simple().with_item(navbar::Item::form(buttons)),
|
||||||
|
"Navbar::simple().with_item(navbar::Item::form(Form::new()...))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_text",
|
||||||
|
Navbar::simple().with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))),
|
||||||
|
"Navbar::simple().with_item(navbar::Item::text(Lc::t(...)))",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_brand_and_text",
|
||||||
|
Navbar::simple()
|
||||||
|
.with_item(navbar::Item::brand(demo_brand()))
|
||||||
|
.with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))),
|
||||||
|
concat!(
|
||||||
|
"Navbar::simple().with_item(navbar::Item::brand(brand))",
|
||||||
|
".with_item(navbar::Item::text(...))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_nav_text",
|
||||||
|
Navbar::brand_left(demo_brand())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(
|
||||||
|
demo_nav().with_prop(Margin::new().with_end(UnitValue::Auto)),
|
||||||
|
))
|
||||||
|
.with_item(navbar::Item::text(Lc::t("menus_text_inline", &LOC))),
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand).with_item(navbar::Item::nav(",
|
||||||
|
"Nav::new().with_prop(Margin::new().with_end(UnitValue::Auto))...))",
|
||||||
|
".with_item(navbar::Item::text(...))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// El núcleo no tiene un componente de grupo de entrada, así que esta demostración usa clases de
|
||||||
|
// Bootstrap y sólo se muestra con el tema Bootsier.
|
||||||
|
if is_bootsier() {
|
||||||
|
let input_group = Nav::new().with_item(nav::Item::html(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
form {
|
||||||
|
div class="input-group" {
|
||||||
|
span class="input-group-text" { "@" }
|
||||||
|
input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder=[Lc::t("menus_group_placeholder", &LOC).lookup(cx)]
|
||||||
|
aria-label=[Lc::t("menus_group_placeholder", &LOC).lookup(cx)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
content_variants.insert(
|
||||||
|
2,
|
||||||
|
(
|
||||||
|
"menus_title_form_group",
|
||||||
|
Navbar::simple().with_item(navbar::Item::nav(input_group)),
|
||||||
|
"Navbar::simple().with_item(navbar::Item::nav(Nav::new()...nav::Item::html(...)))",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_content", &LOC));
|
||||||
|
for (title_key, navbar, code) in content_variants {
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t(title_key, &LOC),
|
||||||
|
code,
|
||||||
|
navbar.with_prop(light_bg()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Esquemas de color >**************************************************************************
|
||||||
|
|
||||||
|
fn colors_block() -> Block {
|
||||||
|
let color_variants: [(&str, Vec<PropsOp>, &str); 4] = [
|
||||||
|
(
|
||||||
|
"menus_title_color_dark",
|
||||||
|
vec![
|
||||||
|
PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)),
|
||||||
|
PropsOp::set("data-bs-theme", "dark"),
|
||||||
|
],
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
".with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)))",
|
||||||
|
".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_color_primary",
|
||||||
|
vec![
|
||||||
|
PropsOp::add_classes(class::Bg::with(BootsierColors::Primary)),
|
||||||
|
PropsOp::set("data-bs-theme", "dark"),
|
||||||
|
],
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
".with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Primary)))",
|
||||||
|
".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_color_light",
|
||||||
|
vec![light_bg()],
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
".with_prop(PropsOp::add_classes(class::Bg::with(class::BgColor::BodyTertiary)))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_color_custom",
|
||||||
|
vec![PropsOp::add_style("background-color", "#e3f2fd")],
|
||||||
|
concat!(
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
".with_prop(PropsOp::add_style(\"background-color\", \"#e3f2fd\"))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_colors", &LOC));
|
||||||
|
for (title_key, props, code) in color_variants {
|
||||||
|
let mut navbar = Navbar::brand_left(demo_brand())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(demo_nav()));
|
||||||
|
for op in props {
|
||||||
|
navbar = navbar.with_prop(op);
|
||||||
|
}
|
||||||
|
block = add_demo(block, Lc::t(title_key, &LOC), code, navbar);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Contenedores >*******************************************************************************
|
||||||
|
|
||||||
|
fn container_block() -> Block {
|
||||||
|
let navbar = || {
|
||||||
|
Navbar::brand_left(demo_brand())
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(demo_nav()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let block = Block::new().with_title(Lc::t("menus_block_title_container", &LOC));
|
||||||
|
let block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t("menus_title_container_full", &LOC),
|
||||||
|
"Navbar::brand_left(brand)...",
|
||||||
|
navbar(),
|
||||||
|
);
|
||||||
|
add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t("menus_title_container_max", &LOC),
|
||||||
|
concat!(
|
||||||
|
"Container::new()",
|
||||||
|
".with_width(container::Width::FluidMax(UnitValue::RelRem(40.0)))",
|
||||||
|
".with_child(Navbar::brand_left(brand)...)",
|
||||||
|
),
|
||||||
|
Container::new()
|
||||||
|
.with_width(container::Width::FluidMax(UnitValue::RelRem(40.0)))
|
||||||
|
.with_child(navbar()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Posición >***********************************************************************************
|
||||||
|
|
||||||
|
fn position_block() -> Block {
|
||||||
|
let navbar = |position| {
|
||||||
|
Navbar::simple()
|
||||||
|
.with_position(position)
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_item(navbar::Item::nav(demo_nav()))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Las barras fijas se anclan al marco y no a la ventana, para no tapar el resto de la página.
|
||||||
|
let fixed_variants: [(&str, navbar::Position, &str); 2] = [
|
||||||
|
(
|
||||||
|
"menus_title_position_fixed_top",
|
||||||
|
navbar::Position::FixedTop,
|
||||||
|
"Navbar::simple().with_position(navbar::Position::FixedTop)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_position_fixed_bottom",
|
||||||
|
navbar::Position::FixedBottom,
|
||||||
|
"Navbar::simple().with_position(navbar::Position::FixedBottom)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new()
|
||||||
|
.with_title(Lc::t("menus_block_title_position", &LOC))
|
||||||
|
.with_child(Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p { (Lc::t("menus_position_note", &LOC).using(cx)) }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
for (title_key, position, code) in fixed_variants {
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t(title_key, &LOC),
|
||||||
|
code,
|
||||||
|
Container::new()
|
||||||
|
.with_width(container::Width::Fluid)
|
||||||
|
.with_prop(PropsOp::add_classes("menus-demo-frame"))
|
||||||
|
.with_child(navbar(position))
|
||||||
|
.with_child(filler()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Con `sticky` la barra ocupa su lugar en el flujo y se queda pegada al borde del contenedor
|
||||||
|
// que se desplaza (aquí un marco con scroll).
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t("menus_title_position_sticky_top", &LOC),
|
||||||
|
"Navbar::simple().with_position(navbar::Position::StickyTop)",
|
||||||
|
Container::new()
|
||||||
|
.with_width(container::Width::Fluid)
|
||||||
|
.with_prop(PropsOp::add_classes("menus-demo-scroll"))
|
||||||
|
.with_child(navbar(navbar::Position::StickyTop))
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler()),
|
||||||
|
);
|
||||||
|
add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t("menus_title_position_sticky_bottom", &LOC),
|
||||||
|
"Navbar::simple().with_position(navbar::Position::StickyBottom)",
|
||||||
|
Container::new()
|
||||||
|
.with_width(container::Width::Fluid)
|
||||||
|
.with_prop(PropsOp::add_classes("menus-demo-scroll"))
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(filler())
|
||||||
|
.with_child(navbar(navbar::Position::StickyBottom)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Disposición >********************************************************************************
|
||||||
|
|
||||||
|
fn layout_block() -> Block {
|
||||||
|
let layout_variants: [(&str, Navbar, &str); 4] = [
|
||||||
|
(
|
||||||
|
"menus_title_layout_toggle",
|
||||||
|
Navbar::simple_toggle(),
|
||||||
|
"Navbar::simple_toggle()",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_layout_brand_left",
|
||||||
|
Navbar::brand_left(demo_brand()),
|
||||||
|
"Navbar::brand_left(brand)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_layout_brand_right",
|
||||||
|
Navbar::brand_right(demo_brand()),
|
||||||
|
"Navbar::brand_right(brand)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_layout_brand_always",
|
||||||
|
Navbar::simple_brand_left(demo_brand()),
|
||||||
|
"Navbar::simple_brand_left(brand)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_layout", &LOC));
|
||||||
|
for (title_key, navbar, code) in layout_variants {
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
Lc::t(title_key, &LOC),
|
||||||
|
code,
|
||||||
|
navbar
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(demo_nav())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Punto de corte >*****************************************************************************
|
||||||
|
|
||||||
|
fn expand_block() -> Block {
|
||||||
|
let expand_variants: [(Lc, Breakpoint, &str); 5] = [
|
||||||
|
(
|
||||||
|
expand_title("sm", "576px"),
|
||||||
|
Breakpoint::Sm,
|
||||||
|
"Navbar::brand_left(brand).with_expand(Breakpoint::Sm)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
expand_title("md", "768px"),
|
||||||
|
Breakpoint::Md,
|
||||||
|
"Navbar::brand_left(brand).with_expand(Breakpoint::Md)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
expand_title("lg", "992px"),
|
||||||
|
Breakpoint::Lg,
|
||||||
|
"Navbar::brand_left(brand).with_expand(Breakpoint::Lg)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
expand_title("xl", "1200px"),
|
||||||
|
Breakpoint::Xl,
|
||||||
|
"Navbar::brand_left(brand).with_expand(Breakpoint::Xl)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
expand_title("xxl", "1400px"),
|
||||||
|
Breakpoint::Xxl,
|
||||||
|
"Navbar::brand_left(brand).with_expand(Breakpoint::Xxl)",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_expand", &LOC));
|
||||||
|
for (title, breakpoint, code) in expand_variants {
|
||||||
|
block = add_demo(
|
||||||
|
block,
|
||||||
|
title,
|
||||||
|
code,
|
||||||
|
Navbar::brand_left(demo_brand())
|
||||||
|
.with_prop(light_bg())
|
||||||
|
.with_expand(breakpoint)
|
||||||
|
.with_item(navbar::Item::nav(demo_nav())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_title(name: &'static str, width: &'static str) -> Lc {
|
||||||
|
Lc::t("menus_title_expand", &LOC)
|
||||||
|
.with_arg("name", name)
|
||||||
|
.with_arg("width", width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Offcanvas >**********************************************************************************
|
||||||
|
|
||||||
|
fn offcanvas_block() -> Block {
|
||||||
|
let panel = |placement, backdrop| {
|
||||||
|
bs::navbar::Panel::new()
|
||||||
|
.with_title(Lc::t("menus_offcanvas_title", &LOC))
|
||||||
|
.with_placement(placement)
|
||||||
|
.with_backdrop(backdrop)
|
||||||
|
};
|
||||||
|
|
||||||
|
let offcanvas_variants: [(&str, Navbar, &str); 4] = [
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_start",
|
||||||
|
Navbar::offcanvas(panel(
|
||||||
|
bs::offcanvas::Placement::Start,
|
||||||
|
bs::offcanvas::Backdrop::Enabled,
|
||||||
|
)),
|
||||||
|
concat!(
|
||||||
|
"Navbar::offcanvas(bs::navbar::Panel::new()",
|
||||||
|
".with_placement(bs::offcanvas::Placement::Start))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_brand_left",
|
||||||
|
Navbar::offcanvas_brand_left(
|
||||||
|
demo_brand(),
|
||||||
|
panel(
|
||||||
|
bs::offcanvas::Placement::End,
|
||||||
|
bs::offcanvas::Backdrop::Enabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()",
|
||||||
|
".with_placement(bs::offcanvas::Placement::End))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_brand_right",
|
||||||
|
Navbar::offcanvas_brand_right(
|
||||||
|
demo_brand(),
|
||||||
|
panel(
|
||||||
|
bs::offcanvas::Placement::Start,
|
||||||
|
bs::offcanvas::Backdrop::Disabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
concat!(
|
||||||
|
"Navbar::offcanvas_brand_right(brand, bs::navbar::Panel::new()",
|
||||||
|
".with_backdrop(bs::offcanvas::Backdrop::Disabled))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"menus_title_offcanvas_dark",
|
||||||
|
Navbar::offcanvas_brand_left(
|
||||||
|
demo_brand(),
|
||||||
|
panel(
|
||||||
|
bs::offcanvas::Placement::End,
|
||||||
|
bs::offcanvas::Backdrop::Static,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_prop(PropsOp::add_classes(class::Bg::with(BootsierColors::Dark)))
|
||||||
|
.with_prop(PropsOp::set("data-bs-theme", "dark")),
|
||||||
|
concat!(
|
||||||
|
"Navbar::offcanvas_brand_left(brand, bs::navbar::Panel::new()",
|
||||||
|
".with_backdrop(bs::offcanvas::Backdrop::Static))",
|
||||||
|
".with_prop(PropsOp::set(\"data-bs-theme\", \"dark\"))",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::new().with_title(Lc::t("menus_block_title_offcanvas", &LOC));
|
||||||
|
for (title_key, navbar, code) in offcanvas_variants {
|
||||||
|
let navbar = navbar
|
||||||
|
.with_expand(Breakpoint::Lg)
|
||||||
|
.with_item(navbar::Item::nav(demo_nav()));
|
||||||
|
// La barra oscura ya define su propio fondo.
|
||||||
|
let navbar = if title_key == "menus_title_offcanvas_dark" {
|
||||||
|
navbar
|
||||||
|
} else {
|
||||||
|
navbar.with_prop(light_bg())
|
||||||
|
};
|
||||||
|
block = add_demo(block, Lc::t(title_key, &LOC), code, navbar);
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HELPERS >************************************************************************************
|
||||||
|
|
||||||
|
// Marca de identidad de las demostraciones.
|
||||||
|
fn demo_brand() -> Brand {
|
||||||
|
Brand::new()
|
||||||
|
.with_title(Lc::n("PageTop"))
|
||||||
|
.with_route(Route::from("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logotipo de las demostraciones, con el tamaño habitual de una marca en una barra.
|
||||||
|
fn demo_logo() -> Image {
|
||||||
|
Image::with(image::Source::logo(PageTopSvg::Tile))
|
||||||
|
.with_size(image::Size::Both(UnitValue::Px(36)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menú común a la mayoría de las demostraciones: activo, enlace, desplegable y deshabilitado.
|
||||||
|
fn demo_nav() -> Nav {
|
||||||
|
Nav::new()
|
||||||
|
.with_item(nav::Item::link(Lc::t("menus_item_home", &LOC), "/").with_active(true))
|
||||||
|
.with_item(nav::Item::link(Lc::t("menus_item_link", &LOC), "/link"))
|
||||||
|
.with_item(nav::Item::dropdown(
|
||||||
|
Dropdown::new()
|
||||||
|
.with_title(Lc::t("menus_test_title", &LOC))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_item_action", &LOC),
|
||||||
|
"/action",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_item_another", &LOC),
|
||||||
|
"/another",
|
||||||
|
))
|
||||||
|
.with_item(dropdown::Item::divider())
|
||||||
|
.with_item(dropdown::Item::link(
|
||||||
|
Lc::t("menus_item_something", &LOC),
|
||||||
|
"/something",
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.with_item(nav::Item::link_disabled(
|
||||||
|
Lc::t("menus_item_disabled", &LOC),
|
||||||
|
"#",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_bootsier() -> bool {
|
||||||
|
global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fondo claro para que las barras destaquen sobre el fondo de la página. Basic no da fondo a
|
||||||
|
// `.navbar`, así que con otros temas se usa el color de fondo de Basic (o un gris claro si el tema
|
||||||
|
// activo tampoco lo define).
|
||||||
|
fn light_bg() -> PropsOp {
|
||||||
|
if is_bootsier() {
|
||||||
|
PropsOp::add_classes(class::Bg::with(class::BgColor::BodyTertiary))
|
||||||
|
} else {
|
||||||
|
PropsOp::add_style("background-color", "var(--val-color--bg, #f8f9fa)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Título y fragmento de código que introducen cada demostración, seguidos de ésta.
|
||||||
|
fn add_demo(block: Block, title: Lc, code: &'static str, demo: impl Into<ChildOp>) -> Block {
|
||||||
|
block
|
||||||
|
.with_child(caption(title, Lc::n(code)))
|
||||||
|
.with_child(demo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Párrafo de relleno para que los marcos de demostración tengan contenido que desplazar.
|
||||||
|
fn filler() -> Html {
|
||||||
|
Html::with(|cx| {
|
||||||
|
html! {
|
||||||
|
p { (Lc::t("menus_filler", &LOC).using(cx)) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Título y fragmento de código que introducen cada demostración.
|
||||||
|
fn caption(title: Lc, code: Lc) -> Html {
|
||||||
|
Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h3 class="menus-demo-title" { (title.using(cx)) }
|
||||||
|
p { code class="menus-demo-code" { (code.using(cx)) } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separa el título de cada demostración de la barra (o marco) que le precede.
|
||||||
|
fn demo_title_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(None, "menus-demo-title", [("margin-top", "1.5rem")])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evita que los fragmentos de código largos desborden su contenedor.
|
||||||
|
fn demo_code_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(None, "menus-demo-code", [("overflow-wrap", "anywhere")])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marco de tamaño fijo para las barras con posición `fixed`.
|
||||||
|
fn demo_frame_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"menus-demo-frame",
|
||||||
|
[
|
||||||
|
("position", "relative"),
|
||||||
|
// Un `transform` convierte al marco en el bloque contenedor de sus descendientes con
|
||||||
|
// `position: fixed`, así la barra se ancla al marco en vez de a la ventana.
|
||||||
|
("transform", "translateZ(0)"),
|
||||||
|
("height", "12rem"),
|
||||||
|
("padding", "4rem 1rem"),
|
||||||
|
("overflow", "hidden"),
|
||||||
|
("border", "1px solid #dee2e6"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marco de tamaño fijo con desplazamiento, para las barras con posición `sticky`.
|
||||||
|
fn demo_scroll_styles() -> AssetsOp {
|
||||||
|
AssetsOp::add_responsive_styles(
|
||||||
|
None,
|
||||||
|
"menus-demo-scroll",
|
||||||
|
[
|
||||||
|
("height", "12rem"),
|
||||||
|
("overflow-y", "auto"),
|
||||||
|
("padding", "0 1rem"),
|
||||||
|
("border", "1px solid #dee2e6"),
|
||||||
|
("margin", "0 0 1.5rem"),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::main]
|
#[pagetop::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
Application::prepare(&SuperMenu).run()?.await
|
Application::prepare(&NavbarMenus).await.run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
extensions/pagetop-admin/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "pagetop-admin"
|
||||||
|
description = "Panel de administración extensible para PageTop."
|
||||||
|
version = "0.1.0"
|
||||||
|
categories = ["web-programming", "development-tools"]
|
||||||
|
keywords = ["pagetop", "admin", "cms", "settings", "ssr"]
|
||||||
|
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
pagetop.workspace = true
|
||||||
|
pagetop-seaorm.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
286
extensions/pagetop-admin/src/action.rs
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
//! Tipos de acción que `pagetop-admin` expone para que otras extensiones registren
|
||||||
|
//! secciones, páginas, tareas y acciones en el panel de administración.
|
||||||
|
//!
|
||||||
|
//! El flujo de `registry::build()` despacha estas acciones en este orden:
|
||||||
|
//!
|
||||||
|
//! 1. `DeclareAdminSections` - secciones (agrupaciones del sidebar).
|
||||||
|
//! 2. `DeclareAdminPages` - páginas del panel.
|
||||||
|
//! 3. `DeclareAdminTasks` - tareas (pestañas) locales por página.
|
||||||
|
//! 4. `DeclareAdminActions` - acciones locales (botones) por página.
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::registry::{AdminAction, AdminPage, AdminSection, AdminTask};
|
||||||
|
|
||||||
|
// **< Tipo de callbacks >**************************************************************************
|
||||||
|
|
||||||
|
pub type FnSectionBag = fn(&mut SectionBag);
|
||||||
|
pub type FnPageBag = fn(&mut PageBag);
|
||||||
|
pub type FnTaskBag = fn(&mut TaskBag);
|
||||||
|
pub type FnActionBag = fn(&mut ActionBag);
|
||||||
|
|
||||||
|
// **< Bolsas de declaración >**********************************************************************
|
||||||
|
|
||||||
|
/// Bolsa de secciones para [`DeclareAdminSections`].
|
||||||
|
pub struct SectionBag {
|
||||||
|
pub(crate) sections: Vec<AdminSection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectionBag {
|
||||||
|
/// Añade una sección al registro.
|
||||||
|
pub fn add(&mut self, section: AdminSection) {
|
||||||
|
self.sections.push(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bolsa de páginas para [`DeclareAdminPages`].
|
||||||
|
pub struct PageBag {
|
||||||
|
pub(crate) pages: Vec<AdminPage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageBag {
|
||||||
|
/// Añade una página al registro.
|
||||||
|
pub fn add(&mut self, page: AdminPage) {
|
||||||
|
self.pages.push(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bolsa de tareas para [`DeclareAdminTasks`].
|
||||||
|
pub struct TaskBag {
|
||||||
|
pub(crate) tasks: Vec<AdminTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskBag {
|
||||||
|
/// Añade una tarea local al registro.
|
||||||
|
pub fn add(&mut self, task: AdminTask) {
|
||||||
|
self.tasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bolsa de acciones locales para [`DeclareAdminActions`].
|
||||||
|
pub struct ActionBag {
|
||||||
|
pub(crate) actions: Vec<AdminAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionBag {
|
||||||
|
/// Añade una acción local al registro.
|
||||||
|
pub fn add(&mut self, action: AdminAction) {
|
||||||
|
self.actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< DeclareAdminSections >***********************************************************************
|
||||||
|
|
||||||
|
/// Acción para declarar secciones del panel de administración.
|
||||||
|
///
|
||||||
|
/// Se despacha durante `registry::build()` (dentro de `initialize()`). El callback
|
||||||
|
/// recibe un [`SectionBag`] y puede llamar a `add()` para registrar secciones.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::locale::Lc;
|
||||||
|
/// use pagetop_admin::action::{DeclareAdminSections, SectionBag};
|
||||||
|
/// use pagetop_admin::registry::AdminSection;
|
||||||
|
///
|
||||||
|
/// fn declare_sections(bag: &mut SectionBag) {
|
||||||
|
/// bag.add(AdminSection {
|
||||||
|
/// key: "tools".to_owned(),
|
||||||
|
/// path: "/admin/tools".to_owned(),
|
||||||
|
/// title: Lc::n("Tools"),
|
||||||
|
/// weight: 60,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// // En Extension::actions():
|
||||||
|
/// // DeclareAdminSections::new(declare_sections)
|
||||||
|
/// ```
|
||||||
|
pub struct DeclareAdminSections {
|
||||||
|
f: FnSectionBag,
|
||||||
|
weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionDispatcher for DeclareAdminSections {
|
||||||
|
fn weight(&self) -> Weight {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeclareAdminSections {
|
||||||
|
pub fn new(f: FnSectionBag) -> Self {
|
||||||
|
DeclareAdminSections { f, weight: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, w: Weight) -> Self {
|
||||||
|
self.weight = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch(bag: &mut SectionBag) {
|
||||||
|
dispatch_actions(|action: &Self| (action.f)(bag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< DeclareAdminPages >**************************************************************************
|
||||||
|
|
||||||
|
/// Acción para declarar páginas del panel de administración.
|
||||||
|
///
|
||||||
|
/// Se despacha durante `registry::build()`. El callback recibe un [`PageBag`].
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
/// use pagetop_admin::action::{DeclareAdminPages, PageBag};
|
||||||
|
/// use pagetop_admin::registry::{AdminPage, AdminPageKind};
|
||||||
|
///
|
||||||
|
/// struct ExportPermission;
|
||||||
|
///
|
||||||
|
/// impl Permission for ExportPermission {
|
||||||
|
/// fn key(&self) -> CowStr {
|
||||||
|
/// "tools:export".into()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn declare_pages(bag: &mut PageBag) {
|
||||||
|
/// bag.add(AdminPage {
|
||||||
|
/// path: "/admin/tools/export".to_owned(),
|
||||||
|
/// section: "tools".to_owned(),
|
||||||
|
/// title: Lc::n("Export"),
|
||||||
|
/// description: Some(Lc::n("Export site data.")),
|
||||||
|
/// weight: 0,
|
||||||
|
/// permission: &ExportPermission,
|
||||||
|
/// kind: AdminPageKind::View,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// // En Extension::actions():
|
||||||
|
/// // DeclareAdminPages::new(declare_pages)
|
||||||
|
/// ```
|
||||||
|
pub struct DeclareAdminPages {
|
||||||
|
f: FnPageBag,
|
||||||
|
weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionDispatcher for DeclareAdminPages {
|
||||||
|
fn weight(&self) -> Weight {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeclareAdminPages {
|
||||||
|
pub fn new(f: FnPageBag) -> Self {
|
||||||
|
DeclareAdminPages { f, weight: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, w: Weight) -> Self {
|
||||||
|
self.weight = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch(bag: &mut PageBag) {
|
||||||
|
dispatch_actions(|action: &Self| (action.f)(bag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< DeclareAdminTasks >**************************************************************************
|
||||||
|
|
||||||
|
/// Acción para declarar tareas (pestañas) locales en páginas del panel.
|
||||||
|
///
|
||||||
|
/// Se despacha durante `registry::build()`. El callback recibe un [`TaskBag`].
|
||||||
|
/// Cada [`AdminTask`] indica en `parent_path` la página a la que pertenece.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::locale::Lc;
|
||||||
|
/// use pagetop_admin::action::{DeclareAdminTasks, TaskBag};
|
||||||
|
/// use pagetop_admin::registry::AdminTask;
|
||||||
|
///
|
||||||
|
/// fn declare_tasks(bag: &mut TaskBag) {
|
||||||
|
/// bag.add(AdminTask {
|
||||||
|
/// path: "/admin/tools/export/csv".to_owned(),
|
||||||
|
/// parent_path: "/admin/tools/export".to_owned(),
|
||||||
|
/// title: Lc::n("CSV"),
|
||||||
|
/// weight: 0,
|
||||||
|
/// is_default: true,
|
||||||
|
/// permission: None,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// // En Extension::actions():
|
||||||
|
/// // DeclareAdminTasks::new(declare_tasks)
|
||||||
|
/// ```
|
||||||
|
pub struct DeclareAdminTasks {
|
||||||
|
f: FnTaskBag,
|
||||||
|
weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionDispatcher for DeclareAdminTasks {
|
||||||
|
fn weight(&self) -> Weight {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeclareAdminTasks {
|
||||||
|
pub fn new(f: FnTaskBag) -> Self {
|
||||||
|
DeclareAdminTasks { f, weight: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, w: Weight) -> Self {
|
||||||
|
self.weight = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch(bag: &mut TaskBag) {
|
||||||
|
dispatch_actions(|action: &Self| (action.f)(bag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< DeclareAdminActions >************************************************************************
|
||||||
|
|
||||||
|
/// Acción para declarar acciones locales (botones de acción) en páginas del panel.
|
||||||
|
///
|
||||||
|
/// Se despacha durante `registry::build()`. El callback recibe un [`ActionBag`].
|
||||||
|
/// Cada [`AdminAction`] indica en `for_path` la página en la que aparece el botón.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::locale::Lc;
|
||||||
|
/// use pagetop_admin::action::{DeclareAdminActions, ActionBag};
|
||||||
|
/// use pagetop_admin::registry::AdminAction;
|
||||||
|
///
|
||||||
|
/// fn declare_actions(bag: &mut ActionBag) {
|
||||||
|
/// bag.add(AdminAction {
|
||||||
|
/// url: "/admin/tools/export/new".to_owned(),
|
||||||
|
/// for_path: "/admin/tools/export".to_owned(),
|
||||||
|
/// title: Lc::n("Add export"),
|
||||||
|
/// weight: 0,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// // En Extension::actions():
|
||||||
|
/// // DeclareAdminActions::new(declare_actions)
|
||||||
|
/// ```
|
||||||
|
pub struct DeclareAdminActions {
|
||||||
|
f: FnActionBag,
|
||||||
|
weight: Weight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionDispatcher for DeclareAdminActions {
|
||||||
|
fn weight(&self) -> Weight {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeclareAdminActions {
|
||||||
|
pub fn new(f: FnActionBag) -> Self {
|
||||||
|
DeclareAdminActions { f, weight: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_weight(mut self, w: Weight) -> Self {
|
||||||
|
self.weight = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch(bag: &mut ActionBag) {
|
||||||
|
dispatch_actions(|action: &Self| (action.f)(bag));
|
||||||
|
}
|
||||||
|
}
|
||||||
9
extensions/pagetop-admin/src/component.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
//! Componentes de renderizado del panel de administración.
|
||||||
|
|
||||||
|
mod admin_frame;
|
||||||
|
mod admin_menu;
|
||||||
|
mod config_form;
|
||||||
|
|
||||||
|
pub use admin_frame::AdminFrame;
|
||||||
|
pub use admin_menu::AdminMenu;
|
||||||
|
pub use config_form::ConfigForm;
|
||||||
150
extensions/pagetop-admin/src/component/admin_frame.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::LOCALES_ADMIN;
|
||||||
|
use crate::registry;
|
||||||
|
|
||||||
|
/// Componente de layout principal de una página del panel de administración.
|
||||||
|
///
|
||||||
|
/// Renderiza breadcrumb, encabezado (título + acciones locales), tareas locales (pestañas) y el
|
||||||
|
/// contenido de la página. No incluye ningún menú de navegación: mostrar las secciones del panel
|
||||||
|
/// (el "sidebar" o el "menú superior", según el tema) es responsabilidad del tema que intercepte
|
||||||
|
/// [`CoreTemplates::Admin`](pagetop::core::theme::CoreTemplates::Admin) (por ejemplo
|
||||||
|
/// `pagetop-bootsier`) leyendo directamente [`crate::registry::global()`]. El tema básico de
|
||||||
|
/// PageTop no lo hace, y una página de administración sigue siendo completamente navegable sin él,
|
||||||
|
/// a través del *dashboard* (`/admin`), las páginas de sección y este mismo breadcrumb.
|
||||||
|
///
|
||||||
|
/// ```html
|
||||||
|
/// <nav class="admin-breadcrumb">...</nav>
|
||||||
|
/// <header class="admin-header">
|
||||||
|
/// <h1 class="admin-page-title">Title</h1>
|
||||||
|
/// <ul class="admin-actions-list">...</ul>
|
||||||
|
/// </header>
|
||||||
|
/// <nav class="admin-local-tasks">...</nav>
|
||||||
|
/// <div class="admin-content">...</div>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// El contenido son sus propios hijos ([`Children`]), igual que en cualquier otro componente
|
||||||
|
/// contenedor (p. ej. [`Block`](pagetop::base::component::Block)): admite tantos `with_child(...)`
|
||||||
|
/// como haga falta, cada uno renderizado en el ciclo normal de componentes.
|
||||||
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
pub struct AdminFrame {
|
||||||
|
/// Devuelve el título de la página.
|
||||||
|
title: Lc,
|
||||||
|
/// Devuelve la lista de componentes hijo de la página.
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for AdminFrame {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
let path = cx
|
||||||
|
.request()
|
||||||
|
.map(|r| r.path().to_string())
|
||||||
|
.unwrap_or_else(|| "/".to_string());
|
||||||
|
|
||||||
|
let breadcrumbs = render_breadcrumbs(cx, &path).await;
|
||||||
|
let local_tasks = render_local_tasks(cx, &path);
|
||||||
|
let local_acts = render_local_actions(cx, &path);
|
||||||
|
let body = self.children().render(cx).await;
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
(breadcrumbs)
|
||||||
|
header class="admin-header" {
|
||||||
|
h1 class="admin-page-title" { (self.title().using(cx)) }
|
||||||
|
(local_acts)
|
||||||
|
}
|
||||||
|
@if !local_tasks.0.is_empty() {
|
||||||
|
nav class="admin-local-tasks" { (local_tasks) }
|
||||||
|
}
|
||||||
|
div class="admin-content" {
|
||||||
|
(body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl AdminFrame {
|
||||||
|
/// Establece el título de la página.
|
||||||
|
pub fn with_title(mut self, title: Lc) -> Self {
|
||||||
|
self.title = title;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un componente hijo al contenido de la página.
|
||||||
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
|
self.children.alter_child(op.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Breadcrumbs >********************************************************************************
|
||||||
|
|
||||||
|
async fn render_breadcrumbs(cx: &mut Context, current_path: &str) -> Markup {
|
||||||
|
let reg = registry::global();
|
||||||
|
let base = crate::ADMIN_BASE_PATH;
|
||||||
|
|
||||||
|
let home_label = Lc::t("admin-breadcrumb-home", &LOCALES_ADMIN);
|
||||||
|
let mut bc = Breadcrumb::new().with_crumb(breadcrumb::Crumb::new(home_label, base));
|
||||||
|
|
||||||
|
if let Some(page) = reg.pages().get(current_path) {
|
||||||
|
if let Some(section) = reg.sections().get(&page.section) {
|
||||||
|
bc = bc.with_crumb(breadcrumb::Crumb::new(
|
||||||
|
section.title.clone(),
|
||||||
|
section.path.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
bc = bc.with_crumb(breadcrumb::Crumb::current(page.title.clone()));
|
||||||
|
} else if let Some(section) = reg.sections().values().find(|s| s.path == current_path) {
|
||||||
|
// La ruta actual es la propia sección (su página de aterrizaje), no una `AdminPage`
|
||||||
|
// registrada dentro de ella.
|
||||||
|
bc = bc.with_crumb(breadcrumb::Crumb::current(section.title.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.render(cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Tareas locales >*****************************************************************************
|
||||||
|
|
||||||
|
fn render_local_tasks(cx: &Context, current_path: &str) -> Markup {
|
||||||
|
let reg = registry::global();
|
||||||
|
let tasks = reg.tasks_for(current_path);
|
||||||
|
if tasks.is_empty() {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
html! {
|
||||||
|
ul class="admin-tasks-list" {
|
||||||
|
@for task in tasks {
|
||||||
|
@let is_active = current_path == task.path;
|
||||||
|
li class=(if is_active { "admin-task admin-task-active" } else { "admin-task" }) {
|
||||||
|
a href=(cx.route(task.path.as_str())) { (task.title.using(cx)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Acciones locales >***************************************************************************
|
||||||
|
|
||||||
|
fn render_local_actions(cx: &Context, current_path: &str) -> Markup {
|
||||||
|
let reg = registry::global();
|
||||||
|
let actions = reg.actions_for(current_path);
|
||||||
|
if actions.is_empty() {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
html! {
|
||||||
|
ul class="admin-actions-list" {
|
||||||
|
@for action in actions {
|
||||||
|
li class="admin-action" {
|
||||||
|
a class="admin-action-link" href=(cx.route(action.url.as_str())) {
|
||||||
|
(action.title.using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
extensions/pagetop-admin/src/component/admin_menu.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::registry;
|
||||||
|
|
||||||
|
/// Componente que renderiza la barra de navegación del panel de administración.
|
||||||
|
///
|
||||||
|
/// Construye la [`Navbar`] en cada petición a partir de [`registry::admin_navbar()`], ya filtrada
|
||||||
|
/// por el usuario de la petición actual. Pensado para que un tema la registre en su propia región
|
||||||
|
/// de navegación e intercepte `Navbar`/`Nav`/`Dropdown` en
|
||||||
|
/// [`Theme::render_component()`](pagetop::core::theme::Theme::render_component) si quiere darle un
|
||||||
|
/// aspecto propio (p. ej. `pagetop-bootsier` la reconoce automáticamente al ser componentes del
|
||||||
|
/// núcleo); sin intercepción, se renderiza con el marcado por defecto de cada componente.
|
||||||
|
///
|
||||||
|
/// Sólo se renderiza en páginas creadas con
|
||||||
|
/// [`Page::admin()`](pagetop::response::Page::admin) (plantilla
|
||||||
|
/// [`CoreTemplates::Admin`](pagetop::core::theme::CoreTemplates::Admin)). Es necesario comprobarlo
|
||||||
|
/// explícitamente porque, si se registra en una región de propósito general como
|
||||||
|
/// [`CoreRegions::Aside`](pagetop::core::theme::CoreRegions::Aside), se renderizaría también en
|
||||||
|
/// páginas `Standard` si no se autolimitara.
|
||||||
|
#[derive(AutoDefault, Clone, Debug)]
|
||||||
|
pub struct AdminMenu;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for AdminMenu {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
if !matches!(
|
||||||
|
cx.template().downcast_ref::<CoreTemplates>(),
|
||||||
|
Some(CoreTemplates::Admin)
|
||||||
|
) {
|
||||||
|
return Ok(html! {});
|
||||||
|
}
|
||||||
|
Ok(registry::admin_navbar(cx).render(cx).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
228
extensions/pagetop-admin/src/component/config_form.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::LOCALES_ADMIN;
|
||||||
|
use crate::settings::{SettingFieldType, SettingsSchema, get_or};
|
||||||
|
|
||||||
|
/// Componente que renderiza un formulario de configuración persistente.
|
||||||
|
///
|
||||||
|
/// Genera campos HTML a partir de un [`SettingsSchema`] y carga los valores
|
||||||
|
/// actuales desde `settings`. El POST es procesado por el handler interno
|
||||||
|
/// `config_form_post`.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
/// use pagetop_admin::component::{AdminFrame, ConfigForm};
|
||||||
|
/// use pagetop_admin::settings::{SettingField, SettingsSchema};
|
||||||
|
///
|
||||||
|
/// async fn settings_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
/// let schema = SettingsSchema::new("myapp.general")
|
||||||
|
/// .with_field(SettingField::text("site_name", "Site name").with_required(true));
|
||||||
|
///
|
||||||
|
/// let title = "General settings";
|
||||||
|
/// let mut form = ConfigForm::with_schema(schema);
|
||||||
|
/// let mut cx = Context::new(request.clone());
|
||||||
|
/// let content = form.render(&mut cx).await;
|
||||||
|
///
|
||||||
|
/// Page::admin(request)
|
||||||
|
/// .with_title(Lc::n(title))
|
||||||
|
/// .with_child(
|
||||||
|
/// AdminFrame::new()
|
||||||
|
/// .with_title(Lc::n(title))
|
||||||
|
/// .with_child(Html::with(move |_| content.clone())),
|
||||||
|
/// )
|
||||||
|
/// .render()
|
||||||
|
/// .await
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
pub struct ConfigForm {
|
||||||
|
/// Devuelve el esquema del formulario, si se ha establecido uno.
|
||||||
|
schema: Option<SettingsSchema>,
|
||||||
|
/// Devuelve la ruta de destino del formulario, si se ha personalizado.
|
||||||
|
action_path: Option<Route>,
|
||||||
|
/// Devuelve si el último envío se guardó correctamente.
|
||||||
|
saved: bool,
|
||||||
|
/// Devuelve si el último envío produjo un error al guardar.
|
||||||
|
error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for ConfigForm {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
let Some(schema) = self.schema() else {
|
||||||
|
return Ok(html! {});
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = match self.action_path() {
|
||||||
|
Some(route) => route.resolve(cx),
|
||||||
|
None => cx.route(cx.request().map(|r| r.path()).unwrap_or("/")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let save_label = Lc::t("config-save-label", &LOCALES_ADMIN)
|
||||||
|
.lookup(cx)
|
||||||
|
.unwrap_or_else(|| "Save configuration".into());
|
||||||
|
|
||||||
|
let saved_msg = Lc::t("config-saved-ok", &LOCALES_ADMIN)
|
||||||
|
.lookup(cx)
|
||||||
|
.unwrap_or_else(|| "Configuration saved.".into());
|
||||||
|
|
||||||
|
let error_msg = Lc::t("config-saved-error", &LOCALES_ADMIN)
|
||||||
|
.lookup(cx)
|
||||||
|
.unwrap_or_else(|| "Could not save configuration.".into());
|
||||||
|
|
||||||
|
// Pre-computa los valores de BD para cada campo antes del macro html! (que es síncrono).
|
||||||
|
struct FieldVals {
|
||||||
|
raw_val: String,
|
||||||
|
num_val: String,
|
||||||
|
checked: bool,
|
||||||
|
}
|
||||||
|
let mut prepared: Vec<FieldVals> = Vec::with_capacity(schema.fields().len());
|
||||||
|
for field in schema.fields() {
|
||||||
|
let key = schema.key_for(field.name());
|
||||||
|
let raw_val =
|
||||||
|
get_or::<String>(&key, field.default_value().cloned().unwrap_or_default()).await;
|
||||||
|
let num_val = get_or::<f64>(&key, 0.0).await.to_string();
|
||||||
|
let checked = get_or::<bool>(&key, false).await;
|
||||||
|
prepared.push(FieldVals {
|
||||||
|
raw_val,
|
||||||
|
num_val,
|
||||||
|
checked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
@if *self.saved() {
|
||||||
|
div class="admin-message admin-message-ok" { (&saved_msg) }
|
||||||
|
}
|
||||||
|
@if *self.error() {
|
||||||
|
div class="admin-message admin-message-error" { (&error_msg) }
|
||||||
|
}
|
||||||
|
form class="admin-config-form" method="post" action=(action) {
|
||||||
|
@for (i, field) in schema.fields().iter().enumerate() {
|
||||||
|
@let pf = &prepared[i];
|
||||||
|
div class="admin-form-field" {
|
||||||
|
label class="admin-form-label" for=(field.name()) { (field.label()) }
|
||||||
|
@match field.field_type() {
|
||||||
|
SettingFieldType::Text { max_length } => {
|
||||||
|
@if let Some(max) = max_length {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="text"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.raw_val)
|
||||||
|
maxlength=(max)
|
||||||
|
required[*field.required()];
|
||||||
|
} @else {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="text"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.raw_val)
|
||||||
|
required[*field.required()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SettingFieldType::Number { min, max } => {
|
||||||
|
@if let (Some(lo), Some(hi)) = (min, max) {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="number"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.num_val)
|
||||||
|
min=(lo)
|
||||||
|
max=(hi)
|
||||||
|
required[*field.required()];
|
||||||
|
} @else if let Some(lo) = min {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="number"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.num_val)
|
||||||
|
min=(lo)
|
||||||
|
required[*field.required()];
|
||||||
|
} @else if let Some(hi) = max {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="number"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.num_val)
|
||||||
|
max=(hi)
|
||||||
|
required[*field.required()];
|
||||||
|
} @else {
|
||||||
|
input class="admin-form-input"
|
||||||
|
type="number"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value=(&pf.num_val)
|
||||||
|
required[*field.required()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SettingFieldType::Boolean => {
|
||||||
|
input class="admin-form-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
value="true"
|
||||||
|
checked[pf.checked];
|
||||||
|
}
|
||||||
|
SettingFieldType::Select { options } => {
|
||||||
|
select class="admin-form-select"
|
||||||
|
id=(field.name())
|
||||||
|
name=(field.name())
|
||||||
|
required[*field.required()]
|
||||||
|
{
|
||||||
|
@for (val, label) in options {
|
||||||
|
@if &pf.raw_val == val {
|
||||||
|
option value=(val) selected { (label) }
|
||||||
|
} @else {
|
||||||
|
option value=(val) { (label) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if let Some(help) = field.help_text() {
|
||||||
|
small class="admin-form-help" { (help) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="admin-form-actions" {
|
||||||
|
button class="admin-btn admin-btn-primary" type="submit" {
|
||||||
|
(&save_label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl ConfigForm {
|
||||||
|
/// Crea el componente con el [`SettingsSchema`] dado.
|
||||||
|
#[builder_skip]
|
||||||
|
pub fn with_schema(schema: SettingsSchema) -> Self {
|
||||||
|
ConfigForm {
|
||||||
|
schema: Some(schema),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_action_path(mut self, v: impl Into<Option<Route>>) -> Self {
|
||||||
|
if let Some(v) = v.into() {
|
||||||
|
self.action_path = Some(v);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_skip]
|
||||||
|
pub(crate) fn with_saved(mut self, saved: bool, error: bool) -> Self {
|
||||||
|
self.saved = saved;
|
||||||
|
self.error = error;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
3
extensions/pagetop-admin/src/entity.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
//! Entidades SeaORM de `pagetop-admin`.
|
||||||
|
|
||||||
|
pub(crate) mod setting;
|
||||||
18
extensions/pagetop-admin/src/entity/setting.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use pagetop_seaorm::db::*;
|
||||||
|
|
||||||
|
/// Entidad SeaORM para la tabla `settings`.
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||||
|
#[sea_orm(table_name = "settings")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub key: String,
|
||||||
|
pub scope: String,
|
||||||
|
pub value: String,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
pub updated_by: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, DeriveRelation, EnumIter)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
14
extensions/pagetop-admin/src/error.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errores que puede producir la extensión `pagetop-admin`.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AdminError {
|
||||||
|
#[error("settings DB error: {0}")]
|
||||||
|
DbError(#[from] pagetop_seaorm::db::DbErr),
|
||||||
|
|
||||||
|
#[error("settings serialization error: {0}")]
|
||||||
|
SerializeError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("setting key not found: {0}")]
|
||||||
|
NotFound(String),
|
||||||
|
}
|
||||||
214
extensions/pagetop-admin/src/handlers.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::LOCALES_ADMIN;
|
||||||
|
use crate::component::{AdminFrame, ConfigForm};
|
||||||
|
use crate::registry::{self, AdminPageKind, AdminSection};
|
||||||
|
use crate::settings;
|
||||||
|
|
||||||
|
// **< Dashboard >**********************************************************************************
|
||||||
|
|
||||||
|
/// GET /admin - Dashboard de administración: lista todas las secciones disponibles.
|
||||||
|
///
|
||||||
|
/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna página.
|
||||||
|
pub async fn dashboard(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
let cx = Context::new(request.clone());
|
||||||
|
let sections = registry::global().ordered_sections();
|
||||||
|
|
||||||
|
let title = Lc::t("dashboard-title", &LOCALES_ADMIN);
|
||||||
|
let content = render_sections(&cx, §ions)
|
||||||
|
.ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?;
|
||||||
|
|
||||||
|
Page::admin(request)
|
||||||
|
.with_title(title.clone())
|
||||||
|
.with_child(AdminFrame::new().with_title(title).with_child(content))
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Sección >************************************************************************************
|
||||||
|
|
||||||
|
/// GET /admin/{section} - Página de aterrizaje de una sección: lista sus páginas.
|
||||||
|
///
|
||||||
|
/// Sólo se monta (ver `configure_router()` en `lib.rs`) para las secciones cuyo `path` no coincida
|
||||||
|
/// con ninguna [`AdminPage`](crate::registry::AdminPage) registrada explícitamente por una
|
||||||
|
/// extensión; si una extensión reclama esa ruta con su propia página, esa página gana y este
|
||||||
|
/// handler nunca llega a montarse para ella.
|
||||||
|
///
|
||||||
|
/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna de sus
|
||||||
|
/// páginas.
|
||||||
|
pub async fn section_page(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
let path = request.path().to_owned();
|
||||||
|
let reg = registry::global();
|
||||||
|
let section = reg
|
||||||
|
.sections()
|
||||||
|
.values()
|
||||||
|
.find(|s| s.path == path)
|
||||||
|
.ok_or_else(|| ErrorPage::NotFound(Some(request.clone())))?;
|
||||||
|
|
||||||
|
let cx = Context::new(request.clone());
|
||||||
|
let title = section.title.clone();
|
||||||
|
let content = render_sections(&cx, &[section])
|
||||||
|
.ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?;
|
||||||
|
|
||||||
|
Page::admin(request)
|
||||||
|
.with_title(title.clone())
|
||||||
|
.with_child(AdminFrame::new().with_title(title).with_child(content))
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Render compartido >**************************************************************************
|
||||||
|
|
||||||
|
// Rejilla de secciones con sus páginas (título + descripción). La usan tanto `dashboard()` (todas
|
||||||
|
// las secciones) como `section_page()` (una sola), filtrando siempre por permiso del usuario
|
||||||
|
// actual. Devuelve `None` si no queda ninguna sección visible con páginas accesibles.
|
||||||
|
fn render_sections(cx: &Context, candidates: &[&'static AdminSection]) -> Option<Container> {
|
||||||
|
let reg = registry::global();
|
||||||
|
let sections_with_pages: Vec<_> = candidates
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|section| {
|
||||||
|
let pages: Vec<_> = reg
|
||||||
|
.pages_for_section(§ion.key)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| p.is_accessible(cx))
|
||||||
|
.collect();
|
||||||
|
(section, pages)
|
||||||
|
})
|
||||||
|
.filter(|(_, pages)| !pages.is_empty())
|
||||||
|
.collect();
|
||||||
|
if sections_with_pages.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut sections = Container::new().with_prop(PropsOp::add_classes("admin-dashboard-sections"));
|
||||||
|
for (section, pages) in sections_with_pages {
|
||||||
|
sections = sections.with_child(
|
||||||
|
Container::new()
|
||||||
|
.with_prop(PropsOp::add_classes("admin-dashboard-section"))
|
||||||
|
.with_child(Html::with(move |cx| {
|
||||||
|
html! {
|
||||||
|
h2 class="admin-dashboard-section-title" {
|
||||||
|
a href=(cx.route(section.path.as_str())) { (section.title.using(cx)) }
|
||||||
|
}
|
||||||
|
ul class="admin-dashboard-section-links" {
|
||||||
|
@for page in &pages {
|
||||||
|
li class="admin-dashboard-link" {
|
||||||
|
a href=(cx.route(page.path.as_str())) {
|
||||||
|
(page.title.using(cx))
|
||||||
|
}
|
||||||
|
@if let Some(description) = &page.description {
|
||||||
|
span class="admin-dashboard-link-desc" {
|
||||||
|
" - " (description.using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Config form - GET >**************************************************************************
|
||||||
|
|
||||||
|
/// GET de una página [`AdminPageKind::ConfigForm`].
|
||||||
|
///
|
||||||
|
/// Lee el schema del registro y renderiza el formulario con los valores actuales.
|
||||||
|
pub async fn config_form_get(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
let path = request.path().to_owned();
|
||||||
|
let reg = registry::global();
|
||||||
|
let page = reg
|
||||||
|
.pages()
|
||||||
|
.get(&path)
|
||||||
|
.ok_or_else(|| ErrorPage::NotFound(Some(request.clone())))?;
|
||||||
|
|
||||||
|
let AdminPageKind::ConfigForm(ref schema) = page.kind else {
|
||||||
|
return Err(ErrorPage::NotFound(Some(request.clone())));
|
||||||
|
};
|
||||||
|
|
||||||
|
require_permission(&request, page.permission)?;
|
||||||
|
|
||||||
|
let title = page.title.clone();
|
||||||
|
let mut form = ConfigForm::with_schema(schema.clone());
|
||||||
|
let mut cx = Context::new(request.clone());
|
||||||
|
let content = form.render(&mut cx).await;
|
||||||
|
|
||||||
|
Page::admin(request)
|
||||||
|
.with_title(title.clone())
|
||||||
|
.with_child(
|
||||||
|
AdminFrame::new()
|
||||||
|
.with_title(title)
|
||||||
|
.with_child(Html::with(move |_| content.clone())),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Config form - POST >*************************************************************************
|
||||||
|
|
||||||
|
/// POST de una página [`AdminPageKind::ConfigForm`].
|
||||||
|
///
|
||||||
|
/// Valida y persiste los valores en `settings`, luego redirige al GET con
|
||||||
|
/// estado `saved=true`.
|
||||||
|
pub async fn config_form_post(
|
||||||
|
request: HttpRequest,
|
||||||
|
web::Form(data): web::Form<HashMap<String, String>>,
|
||||||
|
) -> Result<Markup, ErrorPage> {
|
||||||
|
let path = request.path().to_owned();
|
||||||
|
let reg = registry::global();
|
||||||
|
let page = reg
|
||||||
|
.pages()
|
||||||
|
.get(&path)
|
||||||
|
.ok_or_else(|| ErrorPage::NotFound(Some(request.clone())))?;
|
||||||
|
|
||||||
|
let AdminPageKind::ConfigForm(ref schema) = page.kind else {
|
||||||
|
return Err(ErrorPage::NotFound(Some(request.clone())));
|
||||||
|
};
|
||||||
|
|
||||||
|
require_permission(&request, page.permission)?;
|
||||||
|
|
||||||
|
let mut save_error = false;
|
||||||
|
|
||||||
|
for field in schema.fields() {
|
||||||
|
let key = schema.key_for(field.name());
|
||||||
|
|
||||||
|
// Los checkbox HTML sólo envían el campo si están marcados.
|
||||||
|
let raw = match field.field_type() {
|
||||||
|
crate::settings::SettingFieldType::Boolean => data
|
||||||
|
.get(field.name())
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("false")
|
||||||
|
.to_owned(),
|
||||||
|
_ => {
|
||||||
|
let Some(raw) = data.get(field.name()) else {
|
||||||
|
if *field.required() {
|
||||||
|
save_error = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
raw.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
settings::set::<String>(&key, &raw, schema.scope(), None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = page.title.clone();
|
||||||
|
let mut form = ConfigForm::with_schema(schema.clone()).with_saved(!save_error, save_error);
|
||||||
|
let mut cx = Context::new(request.clone());
|
||||||
|
let content = form.render(&mut cx).await;
|
||||||
|
|
||||||
|
Page::admin(request)
|
||||||
|
.with_title(title.clone())
|
||||||
|
.with_child(
|
||||||
|
AdminFrame::new()
|
||||||
|
.with_title(title)
|
||||||
|
.with_child(Html::with(move |_| content.clone())),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
195
extensions/pagetop-admin/src/lib.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
/*!
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<h1>PageTop Admin</h1>
|
||||||
|
|
||||||
|
<p>Panel de administración extensible para <strong>PageTop</strong>.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Guía rápida
|
||||||
|
|
||||||
|
Declara la dependencia en tu `Cargo.toml` activando el motor de base de datos:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
pagetop-admin = { version = "...", features = ["sqlite"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Añade `&pagetop_admin::Admin` a las dependencias de tu extensión y declara tus
|
||||||
|
secciones, páginas o configuración:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_admin::prelude::*;
|
||||||
|
use pagetop_admin::settings::{SettingField, SettingsSchema};
|
||||||
|
|
||||||
|
pub struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for MyApp {
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_admin::Admin]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actions(&self) -> Vec<ActionBox> {
|
||||||
|
actions![
|
||||||
|
DeclareAdminPages::new(declare_pages),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
router.route("/admin/config/myapp", web::get(config_handler))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declare_pages(bag: &mut PageBag) {
|
||||||
|
bag.add(AdminPage {
|
||||||
|
path: "/admin/config/myapp".to_owned(),
|
||||||
|
section: "config".to_owned(),
|
||||||
|
title: Lc::n("My App"),
|
||||||
|
description: Some(Lc::n("Configure My App.")),
|
||||||
|
weight: 0,
|
||||||
|
permission: &MyPermission::Config,
|
||||||
|
kind: AdminPageKind::View,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum MyPermission {
|
||||||
|
Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permission for MyPermission {
|
||||||
|
fn key(&self) -> CowStr {
|
||||||
|
match self {
|
||||||
|
Self::Config => "myapp:config".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn config_handler(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
let schema = SettingsSchema::new("myapp.config")
|
||||||
|
.with_field(SettingField::text("site_name", "Site name").with_required(true));
|
||||||
|
|
||||||
|
let title = "My App configuration";
|
||||||
|
let mut form = ConfigForm::with_schema(schema);
|
||||||
|
let mut cx = Context::new(request.clone());
|
||||||
|
let content = form.render(&mut cx).await;
|
||||||
|
|
||||||
|
Page::admin(request)
|
||||||
|
.with_title(Lc::n(title))
|
||||||
|
.with_child(
|
||||||
|
AdminFrame::new()
|
||||||
|
.with_title(Lc::n(title))
|
||||||
|
.with_child(Html::with(move |_| content.clone())),
|
||||||
|
)
|
||||||
|
.render()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Para formularios de configuración enteramente automáticos (GET y POST gestionados
|
||||||
|
por `pagetop-admin`), usa `AdminPageKind::ConfigForm(schema)` en lugar de `View` y
|
||||||
|
no registres la ruta tú mismo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::install_migrations;
|
||||||
|
|
||||||
|
include_locales!(LOCALES_ADMIN);
|
||||||
|
|
||||||
|
/// Ruta raíz del panel de administración. No es configurable: otras piezas del ecosistema (p. ej.
|
||||||
|
/// las rutas de `pagetop-user`, `/admin/user/...`) ya asumen este valor de forma literal. Es
|
||||||
|
/// pública para que otras extensiones enlacen al panel sin repetir la ruta.
|
||||||
|
pub const ADMIN_BASE_PATH: &str = "/admin";
|
||||||
|
|
||||||
|
pub mod action;
|
||||||
|
pub mod component;
|
||||||
|
pub mod error;
|
||||||
|
pub mod registry;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
pub(crate) mod entity;
|
||||||
|
pub(crate) mod handlers;
|
||||||
|
pub(crate) mod migration;
|
||||||
|
pub(crate) mod seed;
|
||||||
|
|
||||||
|
pub use action::{
|
||||||
|
ActionBag, DeclareAdminActions, DeclareAdminPages, DeclareAdminSections, DeclareAdminTasks,
|
||||||
|
PageBag, SectionBag, TaskBag,
|
||||||
|
};
|
||||||
|
pub use component::{AdminFrame, AdminMenu, ConfigForm};
|
||||||
|
pub use registry::{AdminAction, AdminPage, AdminPageKind, AdminSection, AdminTask};
|
||||||
|
pub use settings::{SettingField, SettingFieldType, SettingsSchema};
|
||||||
|
|
||||||
|
/// Prelude de `pagetop-admin`.
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::action::{
|
||||||
|
ActionBag, DeclareAdminActions, DeclareAdminPages, DeclareAdminSections, DeclareAdminTasks,
|
||||||
|
PageBag, SectionBag, TaskBag,
|
||||||
|
};
|
||||||
|
pub use crate::component::{AdminFrame, AdminMenu, ConfigForm};
|
||||||
|
pub use crate::error::AdminError;
|
||||||
|
pub use crate::registry::{AdminAction, AdminPage, AdminPageKind, AdminSection, AdminTask};
|
||||||
|
pub use crate::settings::{SettingField, SettingFieldType, SettingsSchema};
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Extension >**********************************************************************************
|
||||||
|
|
||||||
|
/// Implementa la extensión `pagetop-admin`.
|
||||||
|
pub struct Admin;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Extension for Admin {
|
||||||
|
fn name(&self) -> Lc {
|
||||||
|
Lc::t("extension_name", &LOCALES_ADMIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> Lc {
|
||||||
|
Lc::t("extension_description", &LOCALES_ADMIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
|
vec![&pagetop_seaorm::SeaORM]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actions(&self) -> Vec<ActionBox> {
|
||||||
|
actions![action::DeclareAdminSections::new(seed::declare_default_sections).with_weight(-99),]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize(&self) {
|
||||||
|
install_migrations!(m20260629_000001_create_settings);
|
||||||
|
registry::build();
|
||||||
|
|
||||||
|
// Región neutra del core: cualquier tema puede decidir renderizarla (o no) sin que
|
||||||
|
// `pagetop-admin` dependa de ninguno en concreto (ver `CoreRegions::Aside`).
|
||||||
|
InRegion::Global(&CoreRegions::Aside).add(component::AdminMenu::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_router(&self, router: Router) -> Router {
|
||||||
|
let base = ADMIN_BASE_PATH;
|
||||||
|
let reg = registry::global();
|
||||||
|
|
||||||
|
let mut r = router.route(base, web::get(handlers::dashboard));
|
||||||
|
|
||||||
|
// Página de aterrizaje de cada sección, salvo que una extensión ya haya reclamado esa
|
||||||
|
// misma ruta con su propia `AdminPage` (esa página gana y se monta más abajo).
|
||||||
|
for section in reg.sections().values() {
|
||||||
|
if !reg.pages().contains_key(§ion.path) {
|
||||||
|
r = r.route(section.path.as_str(), web::get(handlers::section_page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (path, page) in reg.pages() {
|
||||||
|
if let AdminPageKind::ConfigForm(_) = &page.kind {
|
||||||
|
r = r.route(
|
||||||
|
path.as_str(),
|
||||||
|
web::get(handlers::config_form_get).post(handlers::config_form_post),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
21
extensions/pagetop-admin/src/locale/en-US/common.ftl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
extension_name = PageTop Admin
|
||||||
|
extension_description = Extensible administration panel for PageTop.
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
dashboard-title = Administration
|
||||||
|
dashboard-description = Manage your site.
|
||||||
|
|
||||||
|
# Sections
|
||||||
|
section-people = People
|
||||||
|
section-structure = Structure
|
||||||
|
section-config = Configuration
|
||||||
|
section-reports = Reports
|
||||||
|
section-help = Help
|
||||||
|
|
||||||
|
# Config form
|
||||||
|
config-save-label = Save configuration
|
||||||
|
config-saved-ok = Configuration saved.
|
||||||
|
config-saved-error = Could not save configuration.
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
admin-breadcrumb-home = Administration
|
||||||
21
extensions/pagetop-admin/src/locale/es-ES/common.ftl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
extension_name = PageTop Admin
|
||||||
|
extension_description = Panel de administración extensible para PageTop.
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
dashboard-title = Administración
|
||||||
|
dashboard-description = Gestiona tu sitio.
|
||||||
|
|
||||||
|
# Sections
|
||||||
|
section-people = Personas
|
||||||
|
section-structure = Estructura
|
||||||
|
section-config = Configuración
|
||||||
|
section-reports = Informes
|
||||||
|
section-help = Ayuda
|
||||||
|
|
||||||
|
# Config form
|
||||||
|
config-save-label = Guardar configuración
|
||||||
|
config-saved-ok = Configuración guardada.
|
||||||
|
config-saved-error = No se pudo guardar la configuración.
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
admin-breadcrumb-home = Administración
|
||||||
3
extensions/pagetop-admin/src/migration.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
//! Migraciones de base de datos de `pagetop-admin`.
|
||||||
|
|
||||||
|
pub(crate) mod m20260629_000001_create_settings;
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
use pagetop_seaorm::migration::*;
|
||||||
|
|
||||||
|
/// Tabla `settings`: almacén clave-valor JSON para configuración persistente.
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[pagetop::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Settings::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(string_len(Settings::Key, 190).primary_key())
|
||||||
|
.col(string_len(Settings::Scope, 64))
|
||||||
|
.col(text(Settings::Value))
|
||||||
|
.col(timestamp(Settings::UpdatedAt).default(Expr::current_timestamp()))
|
||||||
|
.col(integer_null(Settings::UpdatedBy))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_settings_scope")
|
||||||
|
.table(Settings::Table)
|
||||||
|
.col(Settings::Scope)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Settings::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Settings {
|
||||||
|
Table,
|
||||||
|
Key,
|
||||||
|
Scope,
|
||||||
|
Value,
|
||||||
|
UpdatedAt,
|
||||||
|
UpdatedBy,
|
||||||
|
}
|
||||||
279
extensions/pagetop-admin/src/registry.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
//! Registro global de secciones, páginas, tareas y acciones del panel de administración.
|
||||||
|
//!
|
||||||
|
//! Se construye una única vez durante `Extension::initialize()` y permanece
|
||||||
|
//! inmutable durante la vida de la aplicación.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::action::{
|
||||||
|
ActionBag, DeclareAdminActions, DeclareAdminPages, DeclareAdminSections, DeclareAdminTasks,
|
||||||
|
PageBag, SectionBag, TaskBag,
|
||||||
|
};
|
||||||
|
use crate::settings::SettingsSchema;
|
||||||
|
|
||||||
|
// **< Tipos del registro >*************************************************************************
|
||||||
|
|
||||||
|
/// Sección del panel de administración (agrupación de páginas en el menú y en el *dashboard*).
|
||||||
|
///
|
||||||
|
/// Una sección no tiene permiso propio: se muestra a quien pueda acceder a alguna de sus páginas, y
|
||||||
|
/// se oculta si no le queda ninguna.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminSection {
|
||||||
|
/// Identificador único de la sección (p. ej. `"config"`).
|
||||||
|
pub key: String,
|
||||||
|
/// Ruta base de la sección (p. ej. `"/admin/config"`).
|
||||||
|
pub path: String,
|
||||||
|
/// Título visible en el sidebar.
|
||||||
|
pub title: Lc,
|
||||||
|
/// Peso para ordenar en el sidebar (menor = antes).
|
||||||
|
pub weight: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Página del panel de administración.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminPage {
|
||||||
|
/// Ruta exacta de la página (p. ej. `"/admin/config/site"`).
|
||||||
|
pub path: String,
|
||||||
|
/// Clave de la sección a la que pertenece.
|
||||||
|
pub section: String,
|
||||||
|
/// Título visible en el sidebar y encabezado de página.
|
||||||
|
pub title: Lc,
|
||||||
|
/// Descripción breve (para el dashboard y listas de páginas), si tiene una.
|
||||||
|
pub description: Option<Lc>,
|
||||||
|
/// Peso dentro de la sección (menor = antes).
|
||||||
|
pub weight: i32,
|
||||||
|
/// Permiso requerido para acceder. Es obligatorio: cada página declara el suyo, y es también
|
||||||
|
/// el que decide si su sección se muestra.
|
||||||
|
pub permission: PermissionRef,
|
||||||
|
/// Tipo de página y datos asociados.
|
||||||
|
pub kind: AdminPageKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminPage {
|
||||||
|
/// Devuelve `true` si el usuario actual puede acceder a esta página.
|
||||||
|
pub fn is_accessible(&self, cx: &Context) -> bool {
|
||||||
|
cx.request()
|
||||||
|
.is_some_and(|request| has_permission(request, self.permission))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variantes de comportamiento de una [`AdminPage`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AdminPageKind {
|
||||||
|
/// Página genérica cuyo handler se registra externamente.
|
||||||
|
View,
|
||||||
|
/// Formulario de configuración gestionado automáticamente por `pagetop-admin`.
|
||||||
|
ConfigForm(SettingsSchema),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tarea (pestaña) local dentro de una página de administración.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminTask {
|
||||||
|
/// Ruta de la tarea.
|
||||||
|
pub path: String,
|
||||||
|
/// Ruta de la página padre.
|
||||||
|
pub parent_path: String,
|
||||||
|
/// Etiqueta de la pestaña.
|
||||||
|
pub title: Lc,
|
||||||
|
/// Peso (menor = primera pestaña).
|
||||||
|
pub weight: i32,
|
||||||
|
/// Si es la tarea por defecto (pestaña activa al entrar a la página padre).
|
||||||
|
pub is_default: bool,
|
||||||
|
/// Permiso requerido (`None` = mismo que la página padre).
|
||||||
|
pub permission: Option<PermissionRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acción local (botón de acción) en una página de administración.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AdminAction {
|
||||||
|
/// Ruta de destino de la acción.
|
||||||
|
pub url: String,
|
||||||
|
/// Página en la que aparece el botón.
|
||||||
|
pub for_path: String,
|
||||||
|
/// Etiqueta del botón.
|
||||||
|
pub title: Lc,
|
||||||
|
/// Peso (menor = primero).
|
||||||
|
pub weight: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< AdminRegistry >******************************************************************************
|
||||||
|
|
||||||
|
/// Registro global del panel de administración, construido una sola vez en `initialize()`.
|
||||||
|
#[derive(Getters)]
|
||||||
|
pub struct AdminRegistry {
|
||||||
|
/// Devuelve las secciones indexadas por clave (`BTreeMap` para orden estable por clave).
|
||||||
|
sections: BTreeMap<String, AdminSection>,
|
||||||
|
/// Devuelve las páginas indexadas por ruta.
|
||||||
|
pages: BTreeMap<String, AdminPage>,
|
||||||
|
/// Devuelve las tareas indexadas por ruta de página padre.
|
||||||
|
tasks: BTreeMap<String, Vec<AdminTask>>,
|
||||||
|
/// Devuelve las acciones indexadas por ruta de página.
|
||||||
|
actions: BTreeMap<String, Vec<AdminAction>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminRegistry {
|
||||||
|
fn new() -> Self {
|
||||||
|
AdminRegistry {
|
||||||
|
sections: BTreeMap::new(),
|
||||||
|
pages: BTreeMap::new(),
|
||||||
|
tasks: BTreeMap::new(),
|
||||||
|
actions: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve las páginas de una sección ordenadas por peso.
|
||||||
|
pub fn pages_for_section(&self, section_key: &str) -> Vec<&AdminPage> {
|
||||||
|
let mut pages: Vec<&AdminPage> = self
|
||||||
|
.pages
|
||||||
|
.values()
|
||||||
|
.filter(|p| p.section == section_key)
|
||||||
|
.collect();
|
||||||
|
pages.sort_by_key(|p| p.weight);
|
||||||
|
pages
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve las secciones ordenadas por peso.
|
||||||
|
pub fn ordered_sections(&self) -> Vec<&AdminSection> {
|
||||||
|
let mut sections: Vec<&AdminSection> = self.sections.values().collect();
|
||||||
|
sections.sort_by_key(|s| s.weight);
|
||||||
|
sections
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve las tareas de una página ordenadas por peso.
|
||||||
|
pub fn tasks_for(&self, path: &str) -> Vec<&AdminTask> {
|
||||||
|
let Some(tasks) = self.tasks.get(path) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let mut t: Vec<&AdminTask> = tasks.iter().collect();
|
||||||
|
t.sort_by_key(|t| t.weight);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve las acciones de una página ordenadas por peso.
|
||||||
|
pub fn actions_for(&self, path: &str) -> Vec<&AdminAction> {
|
||||||
|
let Some(actions) = self.actions.get(path) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let mut a: Vec<&AdminAction> = actions.iter().collect();
|
||||||
|
a.sort_by_key(|a| a.weight);
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Registro global >****************************************************************************
|
||||||
|
|
||||||
|
static REGISTRY: OnceLock<AdminRegistry> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Construye el registro despachando todas las acciones de declaración.
|
||||||
|
///
|
||||||
|
/// Se llama una sola vez desde `Admin::initialize()`.
|
||||||
|
pub(crate) fn build() {
|
||||||
|
let mut registry = AdminRegistry::new();
|
||||||
|
|
||||||
|
// Secciones
|
||||||
|
let mut section_bag = SectionBag {
|
||||||
|
sections: Vec::new(),
|
||||||
|
};
|
||||||
|
DeclareAdminSections::dispatch(&mut section_bag);
|
||||||
|
for s in section_bag.sections {
|
||||||
|
registry.sections.insert(s.key.clone(), s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Páginas
|
||||||
|
let mut page_bag = PageBag { pages: Vec::new() };
|
||||||
|
DeclareAdminPages::dispatch(&mut page_bag);
|
||||||
|
for p in page_bag.pages {
|
||||||
|
registry.pages.insert(p.path.clone(), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tareas
|
||||||
|
let mut task_bag = TaskBag { tasks: Vec::new() };
|
||||||
|
DeclareAdminTasks::dispatch(&mut task_bag);
|
||||||
|
for t in task_bag.tasks {
|
||||||
|
registry
|
||||||
|
.tasks
|
||||||
|
.entry(t.parent_path.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acciones locales
|
||||||
|
let mut action_bag = ActionBag {
|
||||||
|
actions: Vec::new(),
|
||||||
|
};
|
||||||
|
DeclareAdminActions::dispatch(&mut action_bag);
|
||||||
|
for a in action_bag.actions {
|
||||||
|
registry
|
||||||
|
.actions
|
||||||
|
.entry(a.for_path.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
REGISTRY.set(registry).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accede al registro global del panel de administración.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Entra en pánico si se llama antes de que `Admin::initialize()` haya completado.
|
||||||
|
pub fn global() -> &'static AdminRegistry {
|
||||||
|
REGISTRY.get().expect("AdminRegistry not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si el usuario de la petición actual tiene acceso a alguna página del panel, es
|
||||||
|
/// decir, si `/admin` no le devolvería un acceso denegado.
|
||||||
|
///
|
||||||
|
/// Permite a otras extensiones (p. ej. el menú de cuenta de `pagetop-user`) ofrecer el enlace a
|
||||||
|
/// la administración sólo cuando procede.
|
||||||
|
pub fn can_access_admin(cx: &Context) -> bool {
|
||||||
|
let reg = global();
|
||||||
|
reg.ordered_sections().into_iter().any(|section| {
|
||||||
|
reg.pages_for_section(§ion.key)
|
||||||
|
.into_iter()
|
||||||
|
.any(|page| page.is_accessible(cx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Menú de administración >*********************************************************************
|
||||||
|
|
||||||
|
/// Construye la barra de navegación de administración para el usuario de la petición actual: un
|
||||||
|
/// enlace al *dashboard* seguido de un desplegable por sección visible, con sus páginas accesibles
|
||||||
|
/// dentro.
|
||||||
|
///
|
||||||
|
/// Pensado para que un tema la use como navegación de `CoreTemplates::Admin` -- ver
|
||||||
|
/// [`crate::component::AdminMenu`]. `pagetop-admin` no impone ningún marcado propio: la
|
||||||
|
/// [`Navbar`] resultante se renderiza con su aspecto por defecto salvo que el tema lo intercepte
|
||||||
|
/// en [`Theme::render_component()`](pagetop::core::theme::Theme::render_component). Se construye
|
||||||
|
/// enteramente con componentes del núcleo (`Navbar`/`Nav`/`Dropdown`), sin depender de ningún
|
||||||
|
/// tema concreto.
|
||||||
|
pub fn admin_navbar(cx: &Context) -> Navbar {
|
||||||
|
let reg = global();
|
||||||
|
|
||||||
|
let mut nav = Nav::new().with_item(nav::Item::link(
|
||||||
|
Lc::t("dashboard-title", &crate::LOCALES_ADMIN),
|
||||||
|
crate::ADMIN_BASE_PATH,
|
||||||
|
));
|
||||||
|
|
||||||
|
for section in reg.ordered_sections() {
|
||||||
|
let pages: Vec<_> = reg
|
||||||
|
.pages_for_section(§ion.key)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| p.is_accessible(cx))
|
||||||
|
.collect();
|
||||||
|
if pages.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut menu = Dropdown::new().with_title(section.title.clone());
|
||||||
|
for page in pages {
|
||||||
|
menu = menu.with_item(dropdown::Item::link(page.title.clone(), page.path.clone()));
|
||||||
|
}
|
||||||
|
nav = nav.with_item(nav::Item::dropdown(menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
Navbar::simple().with_item(navbar::Item::nav(nav))
|
||||||
|
}
|
||||||
45
extensions/pagetop-admin/src/seed.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::ADMIN_BASE_PATH;
|
||||||
|
use crate::LOCALES_ADMIN;
|
||||||
|
use crate::action::SectionBag;
|
||||||
|
use crate::registry::AdminSection;
|
||||||
|
|
||||||
|
/// Declara las secciones incorporadas del panel de administración.
|
||||||
|
///
|
||||||
|
/// Se registra en `Admin::actions()` como `DeclareAdminSections::new(declare_default_sections)`
|
||||||
|
/// con peso negativo para ejecutarse antes que las extensiones de terceros.
|
||||||
|
pub(crate) fn declare_default_sections(bag: &mut SectionBag) {
|
||||||
|
let base = ADMIN_BASE_PATH;
|
||||||
|
|
||||||
|
bag.add(AdminSection {
|
||||||
|
key: "people".to_owned(),
|
||||||
|
path: format!("{}/people", base),
|
||||||
|
title: Lc::t("section-people", &LOCALES_ADMIN),
|
||||||
|
weight: 10,
|
||||||
|
});
|
||||||
|
bag.add(AdminSection {
|
||||||
|
key: "structure".to_owned(),
|
||||||
|
path: format!("{}/structure", base),
|
||||||
|
title: Lc::t("section-structure", &LOCALES_ADMIN),
|
||||||
|
weight: 20,
|
||||||
|
});
|
||||||
|
bag.add(AdminSection {
|
||||||
|
key: "config".to_owned(),
|
||||||
|
path: format!("{}/config", base),
|
||||||
|
title: Lc::t("section-config", &LOCALES_ADMIN),
|
||||||
|
weight: 30,
|
||||||
|
});
|
||||||
|
bag.add(AdminSection {
|
||||||
|
key: "reports".to_owned(),
|
||||||
|
path: format!("{}/reports", base),
|
||||||
|
title: Lc::t("section-reports", &LOCALES_ADMIN),
|
||||||
|
weight: 40,
|
||||||
|
});
|
||||||
|
bag.add(AdminSection {
|
||||||
|
key: "help".to_owned(),
|
||||||
|
path: format!("{}/help", base),
|
||||||
|
title: Lc::t("section-help", &LOCALES_ADMIN),
|
||||||
|
weight: 50,
|
||||||
|
});
|
||||||
|
}
|
||||||
235
extensions/pagetop-admin/src/settings.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
//! Almacén de configuración persistente basado en `settings`.
|
||||||
|
//!
|
||||||
|
//! Proporciona una API async para leer y escribir valores JSON en la tabla `settings`.
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_seaorm::db::{
|
||||||
|
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, dbconn,
|
||||||
|
};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
use crate::entity::setting::{ActiveModel, Column, Entity};
|
||||||
|
use crate::error::AdminError;
|
||||||
|
|
||||||
|
// **< API pública >********************************************************************************
|
||||||
|
|
||||||
|
/// Lee un valor persistido, devolviendo el `Default` del tipo si no existe.
|
||||||
|
pub async fn get<T: DeserializeOwned + Default>(key: &str) -> T {
|
||||||
|
get_async(key).await.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lee un valor persistido, devolviendo `default` si no existe o hay error.
|
||||||
|
pub async fn get_or<T: DeserializeOwned>(key: &str, default: T) -> T {
|
||||||
|
get_async(key).await.unwrap_or(default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escribe un valor en la tabla `settings`.
|
||||||
|
///
|
||||||
|
/// Si la clave no existe se inserta; si existe, se actualiza.
|
||||||
|
pub async fn set<T: Serialize>(key: &str, value: &T, scope: &str, user_id: Option<i32>) {
|
||||||
|
set_async(key, value, scope, user_id).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elimina una entrada de `settings` por clave.
|
||||||
|
pub async fn delete(key: &str) {
|
||||||
|
delete_async(key).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve todos los pares `(key, value_json)` de un `scope` dado.
|
||||||
|
pub async fn list_scope(scope: &str) -> Vec<(String, String)> {
|
||||||
|
list_scope_async(scope).await.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Implementación asíncrona >*******************************************************************
|
||||||
|
|
||||||
|
async fn get_async<T: DeserializeOwned>(key: &str) -> Result<T, AdminError> {
|
||||||
|
let model = Entity::find_by_id(key)
|
||||||
|
.one(dbconn())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AdminError::NotFound(key.to_owned()))?;
|
||||||
|
Ok(serde_json::from_str(&model.value)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_async<T: Serialize>(
|
||||||
|
key: &str,
|
||||||
|
value: &T,
|
||||||
|
scope: &str,
|
||||||
|
user_id: Option<i32>,
|
||||||
|
) -> Result<(), AdminError> {
|
||||||
|
let value_json = serde_json::to_string(value)?;
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let existing = Entity::find_by_id(key).one(dbconn()).await?;
|
||||||
|
if existing.is_some() {
|
||||||
|
let model = ActiveModel {
|
||||||
|
key: ActiveValue::Unchanged(key.to_owned()),
|
||||||
|
scope: ActiveValue::Set(scope.to_owned()),
|
||||||
|
value: ActiveValue::Set(value_json),
|
||||||
|
updated_at: ActiveValue::Set(now),
|
||||||
|
updated_by: ActiveValue::Set(user_id),
|
||||||
|
};
|
||||||
|
model.update(dbconn()).await?;
|
||||||
|
} else {
|
||||||
|
let model = ActiveModel {
|
||||||
|
key: ActiveValue::Set(key.to_owned()),
|
||||||
|
scope: ActiveValue::Set(scope.to_owned()),
|
||||||
|
value: ActiveValue::Set(value_json),
|
||||||
|
updated_at: ActiveValue::Set(now),
|
||||||
|
updated_by: ActiveValue::Set(user_id),
|
||||||
|
};
|
||||||
|
model.insert(dbconn()).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_async(key: &str) -> Result<(), AdminError> {
|
||||||
|
Entity::delete_by_id(key).exec(dbconn()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_scope_async(scope: &str) -> Result<Vec<(String, String)>, AdminError> {
|
||||||
|
let rows = Entity::find()
|
||||||
|
.filter(Column::Scope.eq(scope))
|
||||||
|
.all(dbconn())
|
||||||
|
.await?;
|
||||||
|
Ok(rows.into_iter().map(|m| (m.key, m.value)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Tipos de esquema >***************************************************************************
|
||||||
|
|
||||||
|
/// Tipo de campo de configuración para un [`SettingsSchema`].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SettingFieldType {
|
||||||
|
/// Texto libre con longitud máxima opcional.
|
||||||
|
Text { max_length: Option<usize> },
|
||||||
|
/// Número (entero o decimal).
|
||||||
|
Number { min: Option<f64>, max: Option<f64> },
|
||||||
|
/// Casilla de verificación (booleano).
|
||||||
|
Boolean,
|
||||||
|
/// Lista de opciones `(valor, etiqueta)`.
|
||||||
|
Select { options: Vec<(String, String)> },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Definición de un campo dentro de un [`SettingsSchema`].
|
||||||
|
#[derive(Clone, Debug, Getters)]
|
||||||
|
pub struct SettingField {
|
||||||
|
/// Devuelve el nombre del campo (clave dentro del scope, p. ej. `"site_name"`).
|
||||||
|
name: String,
|
||||||
|
/// Devuelve la etiqueta visible en el formulario.
|
||||||
|
label: String,
|
||||||
|
/// Devuelve el tipo de campo.
|
||||||
|
field_type: SettingFieldType,
|
||||||
|
/// Devuelve si el campo es obligatorio.
|
||||||
|
required: bool,
|
||||||
|
/// Devuelve el texto de ayuda opcional bajo el campo.
|
||||||
|
help_text: Option<String>,
|
||||||
|
/// Devuelve el valor por defecto como cadena JSON.
|
||||||
|
default_value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl SettingField {
|
||||||
|
/// Crea un campo de texto con nombre y etiqueta.
|
||||||
|
pub fn text(name: impl Into<String>, label: impl Into<String>) -> Self {
|
||||||
|
SettingField {
|
||||||
|
name: name.into(),
|
||||||
|
label: label.into(),
|
||||||
|
field_type: SettingFieldType::Text { max_length: None },
|
||||||
|
required: false,
|
||||||
|
help_text: None,
|
||||||
|
default_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un campo numérico con nombre y etiqueta.
|
||||||
|
pub fn number(name: impl Into<String>, label: impl Into<String>) -> Self {
|
||||||
|
SettingField {
|
||||||
|
name: name.into(),
|
||||||
|
label: label.into(),
|
||||||
|
field_type: SettingFieldType::Number {
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
help_text: None,
|
||||||
|
default_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un campo booleano con nombre y etiqueta.
|
||||||
|
pub fn boolean(name: impl Into<String>, label: impl Into<String>) -> Self {
|
||||||
|
SettingField {
|
||||||
|
name: name.into(),
|
||||||
|
label: label.into(),
|
||||||
|
field_type: SettingFieldType::Boolean,
|
||||||
|
required: false,
|
||||||
|
help_text: None,
|
||||||
|
default_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un campo de selección con nombre, etiqueta y opciones.
|
||||||
|
pub fn select(
|
||||||
|
name: impl Into<String>,
|
||||||
|
label: impl Into<String>,
|
||||||
|
options: Vec<(String, String)>,
|
||||||
|
) -> Self {
|
||||||
|
SettingField {
|
||||||
|
name: name.into(),
|
||||||
|
label: label.into(),
|
||||||
|
field_type: SettingFieldType::Select { options },
|
||||||
|
required: false,
|
||||||
|
help_text: None,
|
||||||
|
default_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece si el campo es obligatorio.
|
||||||
|
pub fn with_required(mut self, required: bool) -> Self {
|
||||||
|
self.required = required;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade texto de ayuda bajo el campo.
|
||||||
|
pub fn with_help(mut self, text: impl Into<String>) -> Self {
|
||||||
|
self.help_text = Some(text.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establece el valor por defecto (como valor JSON serializado).
|
||||||
|
pub fn with_default<T: Serialize>(mut self, value: &T) -> Self {
|
||||||
|
self.default_value = serde_json::to_string(value).ok();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Esquema de formulario de configuración: describe un grupo de ajustes en un scope.
|
||||||
|
#[derive(Clone, Debug, Getters)]
|
||||||
|
pub struct SettingsSchema {
|
||||||
|
/// Devuelve el identificador del grupo (prefijo de las claves en `settings`).
|
||||||
|
scope: String,
|
||||||
|
/// Devuelve los campos del formulario en orden de presentación.
|
||||||
|
fields: Vec<SettingField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[builder_impl]
|
||||||
|
impl SettingsSchema {
|
||||||
|
/// Crea un nuevo esquema vacío para el `scope` dado.
|
||||||
|
pub fn new(scope: impl Into<String>) -> Self {
|
||||||
|
SettingsSchema {
|
||||||
|
scope: scope.into(),
|
||||||
|
fields: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un campo al esquema.
|
||||||
|
pub fn with_field(mut self, field: SettingField) -> Self {
|
||||||
|
self.fields.push(field);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la clave completa de un campo: `"{scope}.{field_name}"`.
|
||||||
|
pub fn key_for(&self, field_name: &str) -> String {
|
||||||
|
util::join!(&self.scope, ".", field_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,5 @@ authors.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pagetop.workspace = true
|
pagetop.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pagetop-build.workspace = true
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,13 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧭 Sobre PageTop
|
## Sobre PageTop
|
||||||
|
|
||||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
[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
|
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||||
configurables, basadas en HTML, CSS y JavaScript.
|
configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
|
## Guía rápida
|
||||||
## ⚡️ Guía rápida
|
|
||||||
|
|
||||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||||
|
|
||||||
|
|
@ -35,6 +34,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyApp {
|
impl Extension for MyApp {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -44,11 +44,6 @@ impl Extension for MyApp {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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:
|
Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
|
|
@ -67,26 +62,24 @@ use pagetop_aliner::Aliner;
|
||||||
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Aliner)
|
.with_theme(&Aliner)
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("sample_title"))
|
.with_title(Lc::l("sample_title"))
|
||||||
.add_child(Html::with(|cx| html! {
|
.with_child(Html::with(|cx| html! {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (Lc::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Advertencia
|
||||||
## 🚧 Advertencia
|
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
**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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
## Licencia
|
||||||
## 📜 Licencia
|
|
||||||
|
|
||||||
El código está disponible bajo una doble licencia:
|
El código está disponible bajo una doble licencia:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@
|
||||||
|
|
||||||
<h1>PageTop Aliner</h1>
|
<h1>PageTop Aliner</h1>
|
||||||
|
|
||||||
<p>Tema para <strong>PageTop</strong> que muestra esquemáticamente la composición de las páginas HTML.</p>
|
<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://docs.rs/pagetop-aliner)
|
||||||
[](https://crates.io/crates/pagetop-aliner)
|
[](https://crates.io/crates/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)
|
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||||
|
|
||||||
<br>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Sobre PageTop
|
## Sobre PageTop
|
||||||
|
|
@ -19,8 +18,7 @@
|
||||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||||
configurables, basadas en HTML, CSS y JavaScript.
|
configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
|
## Guía rápida
|
||||||
# ⚡️ Guía rápida
|
|
||||||
|
|
||||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||||
|
|
||||||
|
|
@ -37,6 +35,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyApp {
|
impl Extension for MyApp {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -46,11 +45,6 @@ impl Extension for MyApp {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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:
|
Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
|
|
@ -71,12 +65,12 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
.with_theme(&Aliner)
|
.with_theme(&Aliner)
|
||||||
.with_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("sample_title"))
|
.with_title(Lc::l("sample_title"))
|
||||||
.with_child(Html::with(|cx| html! {
|
.with_child(Html::with(|cx| html! {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (Lc::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.render()
|
.render().await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
|
|
@ -96,13 +90,14 @@ include_locales!(LOCALES_ALINER);
|
||||||
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
/// - Preparar ejemplos y documentación, sin dependencias visuales (CSS/JS) innecesarias.
|
||||||
pub struct Aliner;
|
pub struct Aliner;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for Aliner {
|
impl Extension for Aliner {
|
||||||
fn name(&self) -> L10n {
|
fn name(&self) -> Lc {
|
||||||
L10n::t("extension_name", &LOCALES_ALINER)
|
Lc::t("extension_name", &LOCALES_ALINER)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> L10n {
|
fn description(&self) -> Lc {
|
||||||
L10n::t("extension_description", &LOCALES_ALINER)
|
Lc::t("extension_description", &LOCALES_ALINER)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(&self) -> Option<ThemeRef> {
|
fn theme(&self) -> Option<ThemeRef> {
|
||||||
|
|
@ -115,25 +110,21 @@ impl Extension for Aliner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Theme for Aliner {
|
impl Theme for Aliner {
|
||||||
fn before_render_page_body(&self, page: &mut Page) {
|
fn before_render_page_body(&self, page: &mut Page) {
|
||||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
page.alter_assets(
|
||||||
StyleSheet::from("/pagetop/css/normalize.css")
|
StyleSheet::from("/pagetop/css/basic.min.css")
|
||||||
.with_version("8.0.1")
|
|
||||||
.with_weight(-99),
|
|
||||||
))
|
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
|
||||||
StyleSheet::from("/pagetop/css/basic.css")
|
|
||||||
.with_version(PAGETOP_VERSION)
|
.with_version(PAGETOP_VERSION)
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
)
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
.alter_assets(
|
||||||
StyleSheet::from("/aliner/css/styles.css")
|
StyleSheet::from("/aliner/css/styles.css")
|
||||||
.with_version(env!("CARGO_PKG_VERSION"))
|
.with_version(env!("CARGO_PKG_VERSION"))
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
)
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Footer,
|
&CoreRegions::Footer,
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,5 @@ authors.workspace = true
|
||||||
pagetop.workspace = true
|
pagetop.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
pagetop-build.workspace = true
|
pagetop-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,13 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🧭 Sobre PageTop
|
## Sobre PageTop
|
||||||
|
|
||||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
[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
|
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||||
configurables, basadas en HTML, CSS y JavaScript.
|
configurables, basadas en HTML, CSS y JavaScript.
|
||||||
|
|
||||||
|
## Guía rápida
|
||||||
## ⚡️ Guía rápida
|
|
||||||
|
|
||||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||||
|
|
||||||
|
|
@ -35,6 +34,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
struct MyApp;
|
struct MyApp;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl Extension for MyApp {
|
impl Extension for MyApp {
|
||||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -44,11 +44,6 @@ impl Extension for MyApp {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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:
|
Y **selecciona el tema en la configuración** de la aplicación:
|
||||||
|
|
@ -67,33 +62,76 @@ use pagetop_bootsier::Bootsier;
|
||||||
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
Page::new(request)
|
Page::new(request)
|
||||||
.with_theme(&Bootsier)
|
.with_theme(&Bootsier)
|
||||||
.add_child(
|
.with_child(
|
||||||
Block::new()
|
Block::new()
|
||||||
.with_title(L10n::l("sample_title"))
|
.with_title(Lc::l("sample_title"))
|
||||||
.add_child(Html::with(|cx| html! {
|
.with_child(Html::with(|cx| html! {
|
||||||
p { (L10n::l("sample_content").using(cx)) }
|
p { (Lc::l("sample_content").using(cx)) }
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
.render().await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plantillas
|
||||||
|
|
||||||
|
Bootsier maqueta las dos plantillas de PageTop (`CoreTemplates`) con la misma cabecera, contenido
|
||||||
|
y pie: `Standard`, la plantilla por defecto de cualquier página; y `Admin`, que se activa creando
|
||||||
|
la página con `Page::admin()` en lugar de `Page::new()`. Ambas se maquetan igual hoy -- la
|
||||||
|
distinción entre página "normal" y de administración no vive en el maquetado, sino en qué
|
||||||
|
extensión registra contenido en cada región y para qué plantilla se autolimita (por ejemplo, el
|
||||||
|
menú de `pagetop-admin`, ver más abajo).
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
async fn about(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||||
|
Page::new(request)
|
||||||
|
.with_child(Html::with(|_| html! {
|
||||||
|
h1 { "Sobre nosotros" }
|
||||||
|
p { "Texto de presentación." }
|
||||||
|
}))
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Menú de administración
|
||||||
|
|
||||||
## 📚 Créditos
|
`pagetop-admin` (si está presente en la aplicación) registra su propio menú de secciones y
|
||||||
|
páginas en `CoreRegions::Aside`, autolimitado a páginas con la plantilla `Admin` -- no requiere
|
||||||
|
ningún registro por parte de Bootsier. Se construye con `Navbar`/`Nav`/`Dropdown`, los mismos
|
||||||
|
componentes disponibles para cualquier página de la aplicación.
|
||||||
|
|
||||||
|
## Selector de modo de color
|
||||||
|
|
||||||
|
`bs::theme_toggle()` crea un ítem de menú con el selector de modo de color (claro / oscuro /
|
||||||
|
automático), pensado para añadirse a cualquier `Nav` de la aplicación, no sólo a las páginas de
|
||||||
|
administración:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
use pagetop_bootsier::theme::*;
|
||||||
|
|
||||||
|
let navbar = bs::Navbar::simple().with_item(bs::navbar::Item::nav(
|
||||||
|
bs::Nav::new()
|
||||||
|
.with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
|
||||||
|
.with_item(bs::theme_toggle()),
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Créditos
|
||||||
|
|
||||||
Este *crate* integra la biblioteca de estilos [Bootstrap 5.3.8](https://getbootstrap.com/) para
|
Este *crate* integra la biblioteca de estilos [Bootstrap 5.3.8](https://getbootstrap.com/) para
|
||||||
definir el comportamiento, la apariencia y los componentes de la interfaz. Bootstrap se distribuye
|
definir el comportamiento, la apariencia y los componentes de la interfaz. Bootstrap se distribuye
|
||||||
bajo licencia [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
bajo licencia [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||||
|
|
||||||
|
## Advertencia
|
||||||
## 🚧 Advertencia
|
|
||||||
|
|
||||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
**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
|
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**.
|
hasta que se libere la versión **1.0.0**.
|
||||||
|
|
||||||
|
## Licencia
|
||||||
## 📜 Licencia
|
|
||||||
|
|
||||||
El código está disponible bajo una doble licencia:
|
El código está disponible bajo una doble licencia:
|
||||||
|
|
||||||
|
|
|
||||||
244
extensions/pagetop-bootsier/assets/_bootsier-custom.scss
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
// Bootsier CSS rules: self-hosted fonts, form elements, regions and components.
|
||||||
|
|
||||||
|
// Self-hosted Source Sans 3 (SIL OFL 1.1), served from /bootsier/fonts. Declared as the primary
|
||||||
|
// font family in $font-family-sans-serif (_bootsier-variables.scss).
|
||||||
|
// Font URLs must match the Preload declarations in src/lib.rs; a mismatch causes double downloads.
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans 3";
|
||||||
|
src: url("/bootsier/fonts/bootsier.font.woff2") format("woff2");
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans 3";
|
||||||
|
src: url("/bootsier/fonts/bootsier.font.italic.woff2") format("woff2");
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font-relative top offset to keep the skip-link hidden at any font size.
|
||||||
|
.skip-link {
|
||||||
|
top: -3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required field indicator in forms.
|
||||||
|
.form-required {
|
||||||
|
color: var(--bs-danger);
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form field with consistent bottom margin.
|
||||||
|
.form-field {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button set (button::ButtonSet): spaces its buttons with a gap instead of relying on Bootstrap's
|
||||||
|
// .btn-group, which merges adjacent buttons into a single joined control.
|
||||||
|
.button-set {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $spacer * .5;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fieldset with border and floating legend.
|
||||||
|
fieldset {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
padding: 2rem 1rem 1rem;
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset > legend {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 1rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius);
|
||||||
|
padding: 0.125rem 0.75rem;
|
||||||
|
font-size: ($font-size-sm + $font-size-base) / 2;
|
||||||
|
font-weight: var(--bs-font-weight-normal);
|
||||||
|
line-height: 1.25;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 75%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset-description {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gap between group label and first inline check.
|
||||||
|
.form-label + .form-check-inline {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer region.
|
||||||
|
.region-footer {
|
||||||
|
padding: 0.75rem 0 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages component. Classes are fixed by pagetop core (`.message`, `.message-info`,
|
||||||
|
// `.message-warning`, `.message-error`); reusing Bootstrap's own `.alert`/`.alert-*` styles keeps
|
||||||
|
// the palette in sync with the active Bootstrap theme instead of duplicating it here.
|
||||||
|
.message {
|
||||||
|
@extend .alert;
|
||||||
|
}
|
||||||
|
.message-info {
|
||||||
|
@extend .alert-info;
|
||||||
|
}
|
||||||
|
.message-warning {
|
||||||
|
@extend .alert-warning;
|
||||||
|
}
|
||||||
|
.message-error {
|
||||||
|
@extend .alert-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort indicator on sortable table headers (`table::SortLink`), using Bootstrap Icons'
|
||||||
|
// sort-up/sort-down glyphs instead of the plain triangle from basic.css.
|
||||||
|
.table-sort::after {
|
||||||
|
font-family: $bootstrap-icons-font;
|
||||||
|
content: "\f575"; // sort-down
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
.table-sort-asc::after {
|
||||||
|
content: "\f57b"; // sort-up
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.table-sort-desc::after {
|
||||||
|
content: "\f575"; // sort-down
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pager component.
|
||||||
|
// `.pagination`, `.page-item`, `.page-link`, `.active` and `.disabled` already come styled by
|
||||||
|
// Bootstrap, since Pager reuses that same markup (the ellipsis is a disabled `.page-link` span,
|
||||||
|
// same as any other cell). Only what Pager adds on top needs rules here: the <nav> wrapper and
|
||||||
|
// the jump-to-page form.
|
||||||
|
|
||||||
|
// <nav> wrapper: flex row that also holds the jump-to-page form.
|
||||||
|
.pager {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0 $spacer; // row-gap column-gap: no vertical gap when the summary drops to its own line.
|
||||||
|
margin-block: $spacer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal alignment set via PagerAlign.
|
||||||
|
.pager-align-start {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.pager-align-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.pager-align-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pushes the rest of .pager (pagination, jump form) to the opposite side. With
|
||||||
|
// PagerAlign::Start the summary swaps to the other end instead, since otherwise it would sit
|
||||||
|
// right next to a pagination that's already at the start. With PagerAlign::Center an auto margin
|
||||||
|
// wouldn't work -it always claims all the free space, so the pagination could never be genuinely
|
||||||
|
// centered- so instead the summary drops to its own line below, centered too.
|
||||||
|
.pager-summary {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
font-size: $font-size-sm;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-inline-end: auto;
|
||||||
|
}
|
||||||
|
.pager-align-start .pager-summary {
|
||||||
|
order: 1;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
.pager-align-center .pager-summary {
|
||||||
|
order: 1;
|
||||||
|
flex-basis: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, .page-link-icon shows the text of pager_previous_label/pager_next_label
|
||||||
|
// ("Previous"/"Next"). The full accessible text lives separately, in the link's aria-label, so
|
||||||
|
// replacing the visible text here with a symbol does not affect accessibility. To use "‹"/"›"
|
||||||
|
// instead, uncomment:
|
||||||
|
/*
|
||||||
|
.page-link-icon {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
.page-previous .page-link-icon::before {
|
||||||
|
content: "‹";
|
||||||
|
font-size: var(--bs-pagination-font-size);
|
||||||
|
display: inline-block;
|
||||||
|
transform: translateY(-0.1em) scale(1.5);
|
||||||
|
}
|
||||||
|
.page-next .page-link-icon::before {
|
||||||
|
content: "›";
|
||||||
|
font-size: var(--bs-pagination-font-size);
|
||||||
|
display: inline-block;
|
||||||
|
transform: translateY(-0.1em) scale(1.5);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Jump-to-page field and submit button, presented as a single joined control. Same block margins
|
||||||
|
// as `ol`/`ul`/`dl` in Bootstrap's reboot (literal values there too, not variables).
|
||||||
|
.pager-jump {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One focus ring for the whole control instead of one per element.
|
||||||
|
.pager-jump:focus-within {
|
||||||
|
border-radius: $input-border-radius;
|
||||||
|
box-shadow: $input-focus-box-shadow;
|
||||||
|
}
|
||||||
|
.pager-jump-input > .form-control:focus,
|
||||||
|
.pager-jump-button:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.pager-jump-input > .form-control {
|
||||||
|
// Extra width equals twice the field's own horizontal padding.
|
||||||
|
width: calc(var(--pager-jump-width, 1ch) + #{$input-padding-x * 4});
|
||||||
|
text-align: center;
|
||||||
|
appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
|
||||||
|
// Logical (not physical) corners, so the shape flips correctly under RTL.
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pager-jump-input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.pager-jump-button {
|
||||||
|
@extend .btn-primary;
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
}
|
||||||
5
extensions/pagetop-bootsier/assets/_bootsier-icons.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Bootstrap Icons v1.13.1, fonts served from /bootsier/fonts.
|
||||||
|
|
||||||
|
$bootstrap-icons-font: "bootsier.icons";
|
||||||
|
$bootstrap-icons-font-dir: "/bootsier/fonts";
|
||||||
|
@import "bootstrap-icons-1.13.1/bootstrap-icons";
|
||||||
|
|
@ -1,80 +1,14 @@
|
||||||
// Enable CSS Grid
|
// Bootstrap utility extensions. Imported just before utilities/api so variable changes take effect.
|
||||||
|
|
||||||
|
// CSS Grid (Bootstrap 5): disable legacy grid classes and enable native CSS Grid.
|
||||||
$enable-grid-classes: false;
|
$enable-grid-classes: false;
|
||||||
$enable-cssgrid: true;
|
$enable-cssgrid: true;
|
||||||
|
|
||||||
// Opacity
|
// Extend the $utilities map with additional classes.
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FORMS
|
|
||||||
|
|
||||||
// Required field indicator
|
|
||||||
.form-required {
|
|
||||||
color: var(--bs-danger);
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
.form-field {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fieldset
|
|
||||||
fieldset {
|
|
||||||
position: relative;
|
|
||||||
background-color: var(--bs-body-bg);
|
|
||||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
||||||
border-radius: var(--bs-border-radius);
|
|
||||||
padding: 2rem 1rem 1rem;
|
|
||||||
margin: 2rem 0 1rem;
|
|
||||||
}
|
|
||||||
fieldset > legend {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 1rem;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background-color: var(--bs-body-bg);
|
|
||||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
|
||||||
border-radius: var(--bs-border-radius);
|
|
||||||
padding: 0.125rem 0.75rem;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
line-height: 1.25;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 75%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.fieldset-description {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check buttons, gap between label and first inline check
|
|
||||||
.form-label + .form-check-inline {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extending utilities
|
|
||||||
$utilities: map-merge(
|
$utilities: map-merge(
|
||||||
$utilities,
|
$utilities,
|
||||||
(
|
(
|
||||||
// Individual border widths
|
// Individual border widths per side.
|
||||||
"border-top": (
|
"border-top": (
|
||||||
property: border-top-width,
|
property: border-top-width,
|
||||||
class: border-top,
|
class: border-top,
|
||||||
|
|
@ -95,7 +29,7 @@ $utilities: map-merge(
|
||||||
class: border-start,
|
class: border-start,
|
||||||
values: $border-widths
|
values: $border-widths
|
||||||
),
|
),
|
||||||
// Individual rounded values
|
// Individual corner radii.
|
||||||
"rounded-top-start": (
|
"rounded-top-start": (
|
||||||
property: border-top-left-radius,
|
property: border-top-left-radius,
|
||||||
class: rounded-top-start,
|
class: rounded-top-start,
|
||||||
|
|
@ -156,11 +90,18 @@ $utilities: map-merge(
|
||||||
pill: var(--#{$prefix}border-radius-pill)
|
pill: var(--#{$prefix}border-radius-pill)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
// Opacity: add missing values (0 for bg/border/text; 10 for text) not in Bootstrap defaults.
|
||||||
|
"bg-opacity": map-merge(
|
||||||
|
map-get($utilities, "bg-opacity"),
|
||||||
|
(values: map-merge(map-get(map-get($utilities, "bg-opacity"), "values"), (0: 0)))
|
||||||
|
),
|
||||||
|
"border-opacity": map-merge(
|
||||||
|
map-get($utilities, "border-opacity"),
|
||||||
|
(values: map-merge(map-get(map-get($utilities, "border-opacity"), "values"), (0: 0)))
|
||||||
|
),
|
||||||
|
"text-opacity": map-merge(
|
||||||
|
map-get($utilities, "text-opacity"),
|
||||||
|
(values: map-merge(map-get(map-get($utilities, "text-opacity"), "values"), (0: 0, 10: .1)))
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Region Footer
|
|
||||||
.region-footer {
|
|
||||||
padding: .75rem 0 3rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
11
extensions/pagetop-bootsier/assets/_bootsier-variables.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Bootsier overrides for Bootstrap variables. Imported before bootstrap/scss/variables
|
||||||
|
// so that Bootstrap's !default declarations do not override these values.
|
||||||
|
|
||||||
|
$font-size-base: 1.125rem;
|
||||||
|
$navbar-brand-font-size: 1.75rem;
|
||||||
|
|
||||||
|
// Self-hosted Source Sans 3 (SIL OFL 1.1) as the primary font family; see the @font-face
|
||||||
|
// declarations in _bootsier-custom.scss.
|
||||||
|
$font-family-sans-serif: "Source Sans 3", system-ui, -apple-system, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
62
extensions/pagetop-bootsier/assets/bootsier.confirm.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// htmx dispatches `htmx:confirm` in place of `window.confirm()` whenever the element that
|
||||||
|
// would issue a request carries `hx-confirm`. Intercepting it here upgrades every
|
||||||
|
// `hx-confirm` in the project to a Bootstrap modal, without touching the element that
|
||||||
|
// requested it or the request itself: cancelling is a no-op, confirming resumes the exact
|
||||||
|
// same request via `evt.detail.issueRequest(true)`.
|
||||||
|
if (typeof bootstrap === 'undefined' || !bootstrap.Modal) { return; }
|
||||||
|
|
||||||
|
var modalEl = null;
|
||||||
|
var questionEl = null;
|
||||||
|
var pendingEvent = null;
|
||||||
|
|
||||||
|
function ensureModal() {
|
||||||
|
if (modalEl) { return; }
|
||||||
|
|
||||||
|
var okLabel = document.body.dataset.confirmOk || 'OK';
|
||||||
|
var cancelLabel = document.body.dataset.confirmCancel || 'Cancel';
|
||||||
|
|
||||||
|
modalEl = document.createElement('div');
|
||||||
|
modalEl.className = 'modal fade';
|
||||||
|
modalEl.tabIndex = -1;
|
||||||
|
modalEl.setAttribute('aria-hidden', 'true');
|
||||||
|
modalEl.innerHTML =
|
||||||
|
'<div class="modal-dialog">' +
|
||||||
|
'<div class="modal-content">' +
|
||||||
|
'<div class="modal-body"></div>' +
|
||||||
|
'<div class="modal-footer">' +
|
||||||
|
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>' +
|
||||||
|
'<button type="button" class="btn btn-danger" data-confirm-accept></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(modalEl);
|
||||||
|
|
||||||
|
questionEl = modalEl.querySelector('.modal-body');
|
||||||
|
modalEl.querySelector('[data-bs-dismiss]').textContent = cancelLabel;
|
||||||
|
var acceptBtn = modalEl.querySelector('[data-confirm-accept]');
|
||||||
|
acceptBtn.textContent = okLabel;
|
||||||
|
|
||||||
|
acceptBtn.addEventListener('click', function () {
|
||||||
|
var evt = pendingEvent;
|
||||||
|
pendingEvent = null;
|
||||||
|
bootstrap.Modal.getInstance(modalEl).hide();
|
||||||
|
if (evt) { evt.detail.issueRequest(true); }
|
||||||
|
});
|
||||||
|
|
||||||
|
modalEl.addEventListener('hidden.bs.modal', function () {
|
||||||
|
pendingEvent = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('htmx:confirm', function (evt) {
|
||||||
|
if (!evt.detail.question) { return; }
|
||||||
|
evt.preventDefault();
|
||||||
|
ensureModal();
|
||||||
|
questionEl.textContent = evt.detail.question;
|
||||||
|
pendingEvent = evt;
|
||||||
|
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||||
|
});
|
||||||
|
}());
|
||||||
18
extensions/pagetop-bootsier/assets/bootsier.dialog.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Bootstrap's `.modal` is a plain `position: fixed` element: it never leaves the CSS stacking
|
||||||
|
// context of whatever ancestor it was rendered inside. If any ancestor sets its own `z-index`
|
||||||
|
// (e.g. pagetop's `Intro` component, for its decorative layering), the modal's `z-index` only
|
||||||
|
// ranks it among the elements of *that* ancestor's context, so `.modal-backdrop` -always a
|
||||||
|
// direct child of `<body>`, added by Bootstrap itself- can end up rendering above it and
|
||||||
|
// swallowing every click, even though `.modal`'s own `z-index` is numerically higher. Basic's
|
||||||
|
// native `<dialog>` escapes this for free via the browser's "top layer"; Bootsier's `.modal`
|
||||||
|
// does not, so it is moved to `<body>` here, exactly like Bootstrap already does with its own
|
||||||
|
// backdrop.
|
||||||
|
document.querySelectorAll('.modal.dialog').forEach(function (modal) {
|
||||||
|
if (modal.parentElement !== document.body) {
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}());
|
||||||
67
extensions/pagetop-bootsier/assets/bootsier.scss
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Bootstrap 5.3.8 - PageTop Bootsier Styles.
|
||||||
|
|
||||||
|
// Bootstrap Configuration
|
||||||
|
// ---------------------------------------------------
|
||||||
|
@import "bootstrap-5.3.8/scss/functions";
|
||||||
|
|
||||||
|
// Bootsier Bootstrap variable overrides
|
||||||
|
// ---------------------------------------------------
|
||||||
|
@import "bootsier-variables";
|
||||||
|
|
||||||
|
// Bootstrap Configuration
|
||||||
|
// ---------------------------------------------------
|
||||||
|
@import "bootstrap-5.3.8/scss/variables";
|
||||||
|
@import "bootstrap-5.3.8/scss/variables-dark";
|
||||||
|
@import "bootstrap-5.3.8/scss/maps";
|
||||||
|
@import "bootstrap-5.3.8/scss/mixins";
|
||||||
|
@import "bootstrap-5.3.8/scss/utilities";
|
||||||
|
|
||||||
|
// Bootstrap Layout & components
|
||||||
|
@import "bootstrap-5.3.8/scss/root";
|
||||||
|
@import "bootstrap-5.3.8/scss/reboot";
|
||||||
|
@import "bootstrap-5.3.8/scss/type";
|
||||||
|
@import "bootstrap-5.3.8/scss/images";
|
||||||
|
@import "bootstrap-5.3.8/scss/containers";
|
||||||
|
@import "bootstrap-5.3.8/scss/grid";
|
||||||
|
@import "bootstrap-5.3.8/scss/tables";
|
||||||
|
@import "bootstrap-5.3.8/scss/forms";
|
||||||
|
@import "bootstrap-5.3.8/scss/buttons";
|
||||||
|
@import "bootstrap-5.3.8/scss/transitions";
|
||||||
|
@import "bootstrap-5.3.8/scss/dropdown";
|
||||||
|
@import "bootstrap-5.3.8/scss/button-group";
|
||||||
|
@import "bootstrap-5.3.8/scss/nav";
|
||||||
|
@import "bootstrap-5.3.8/scss/navbar";
|
||||||
|
@import "bootstrap-5.3.8/scss/card";
|
||||||
|
@import "bootstrap-5.3.8/scss/accordion";
|
||||||
|
@import "bootstrap-5.3.8/scss/breadcrumb";
|
||||||
|
@import "bootstrap-5.3.8/scss/pagination";
|
||||||
|
@import "bootstrap-5.3.8/scss/badge";
|
||||||
|
@import "bootstrap-5.3.8/scss/alert";
|
||||||
|
@import "bootstrap-5.3.8/scss/progress";
|
||||||
|
@import "bootstrap-5.3.8/scss/list-group";
|
||||||
|
@import "bootstrap-5.3.8/scss/close";
|
||||||
|
@import "bootstrap-5.3.8/scss/toasts";
|
||||||
|
@import "bootstrap-5.3.8/scss/modal";
|
||||||
|
@import "bootstrap-5.3.8/scss/tooltip";
|
||||||
|
@import "bootstrap-5.3.8/scss/popover";
|
||||||
|
@import "bootstrap-5.3.8/scss/carousel";
|
||||||
|
@import "bootstrap-5.3.8/scss/spinners";
|
||||||
|
@import "bootstrap-5.3.8/scss/offcanvas";
|
||||||
|
@import "bootstrap-5.3.8/scss/placeholders";
|
||||||
|
|
||||||
|
// Bootstrap Helpers
|
||||||
|
@import "bootstrap-5.3.8/scss/helpers";
|
||||||
|
|
||||||
|
// Bootsier Utilities (must precede utilities/api to extend $utilities)
|
||||||
|
@import "bootsier-utilities";
|
||||||
|
|
||||||
|
// Bootstrap Utilities
|
||||||
|
@import "bootstrap-5.3.8/scss/utilities/api";
|
||||||
|
|
||||||
|
// Bootstrap Icons 1.13.1
|
||||||
|
// ---------------------------------------------------
|
||||||
|
@import "bootsier-icons";
|
||||||
|
|
||||||
|
// Bootsier customizations
|
||||||
|
// ---------------------------------------------------
|
||||||
|
@import "bootsier-custom";
|
||||||