diff --git a/CREDITS.md b/CREDITS.md index f61bf867..227257d1 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,9 +1,9 @@ -# 馃攦 Dependencias +# Dependencias PageTop est谩 basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando algunas de las librer铆as m谩s robustas y populares del [ecosistema Rust](https://lib.rs/) como son: - * [Actix Web](https://actix.rs/) para los servicios web. + * [Axum](https://github.com/tokio-rs/axum) para los servicios web. * [Config](https://docs.rs/config) para cargar y procesar las opciones de configuraci贸n. * [Tracing](https://github.com/tokio-rs/tracing) para la gesti贸n de trazas y registro de eventos de la aplicaci贸n. @@ -13,7 +13,7 @@ algunas de las librer铆as m谩s robustas y populares del [ecosistema Rust](https: PageTop y sus extensiones. -# 馃棜 FIGfonts +# FIGfonts PageTop usa el *crate* [figlet-rs](https://crates.io/crates/figlet-rs) desarrollado por *yuanbohan* para mostrar un banner de presentaci贸n en el terminal con el nombre de la aplicaci贸n en caracteres @@ -26,7 +26,13 @@ para mostrar un banner de presentaci贸n en el terminal con el nombre de la aplic * [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck* -# 馃懢 Icono +# Men煤s accesibles + +El tema Basic de PageTop usa [Accessible Menu](https://github.com/NickDJM/accessible-menu), +desarrollado por *Nick Milton*, para implementar la navegaci贸n de sus men煤s con soporte avanzado. + + +# Icono "La Mascota" sonriente es una simp谩tica creaci贸n de [Webalys](https://www.iconfinder.com/webalys). Forma parte de su colecci贸n [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en diff --git a/Cargo.lock b/Cargo.lock index 1ad66c7c..e00901ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,18 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -242,6 +254,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -251,6 +272,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -399,6 +429,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.10.1" @@ -424,6 +460,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -501,6 +546,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.20.11" @@ -541,7 +595,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -583,12 +637,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1053,7 +1118,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1116,6 +1181,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -1489,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1716,6 +1790,7 @@ dependencies = [ name = "pagetop" version = "0.5.0" dependencies = [ + "async-trait", "axum", "chrono", "colored", @@ -1736,6 +1811,7 @@ dependencies = [ "serde_json", "tempfile", "terminal_size", + "thiserror", "tokio", "tower", "tower-http", @@ -1745,6 +1821,17 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "pagetop-admin" +version = "0.1.0" +dependencies = [ + "pagetop", + "pagetop-seaorm", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "pagetop-aliner" version = "0.1.0" @@ -1777,6 +1864,7 @@ version = "0.1.0" dependencies = [ "pagetop", "pagetop-build", + "serde_json", ] [[package]] @@ -1789,6 +1877,19 @@ dependencies = [ "syn", ] +[[package]] +name = "pagetop-menu" +version = "0.1.0" +dependencies = [ + "chrono", + "pagetop", + "pagetop-seaorm", + "sea-orm", + "serde", + "thiserror", + "tokio", +] + [[package]] name = "pagetop-minimal" version = "0.1.0" @@ -1802,7 +1903,6 @@ dependencies = [ name = "pagetop-seaorm" version = "0.0.4" dependencies = [ - "async-trait", "pagetop", "sea-orm", "sea-schema", @@ -1820,6 +1920,24 @@ dependencies = [ "path-slash 0.2.1", ] +[[package]] +name = "pagetop-user" +version = "0.1.0" +dependencies = [ + "argon2", + "base64ct", + "pagetop", + "pagetop-admin", + "pagetop-htmx", + "pagetop-seaorm", + "rand_core", + "serde", + "serde_json", + "serde_qs", + "sha2 0.11.0", + "thiserror", +] + [[package]] name = "parking" version = "2.2.1" @@ -1862,6 +1980,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "pastey" version = "0.2.2" @@ -2161,8 +2290,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -2260,6 +2389,7 @@ checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" dependencies = [ "async-stream", "async-trait", + "chrono", "derive_more", "futures-util", "log", @@ -2295,6 +2425,7 @@ version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" dependencies = [ + "chrono", "inherent", "ordered-float", "sea-query-derive", @@ -2306,6 +2437,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ + "chrono", "sea-query", "sqlx", ] @@ -2436,6 +2568,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_qs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332" +dependencies = [ + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -2464,8 +2608,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -2475,8 +2619,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2510,7 +2665,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core", ] @@ -2591,6 +2746,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -2609,7 +2765,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror", "tokio", @@ -2646,7 +2802,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -2667,8 +2823,9 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -2689,7 +2846,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -2708,6 +2865,7 @@ dependencies = [ "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2726,7 +2884,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -2742,6 +2900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -3044,9 +3203,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.11" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" dependencies = [ "bitflags", "bytes", @@ -3065,6 +3224,7 @@ dependencies = [ "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 03a5944a..730922ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,10 @@ members = [ "extensions/pagetop-aliner", "extensions/pagetop-bootsier", "extensions/pagetop-htmx", + "extensions/pagetop-admin", + "extensions/pagetop-menu", "extensions/pagetop-seaorm", + "extensions/pagetop-user", ] [workspace.package] @@ -21,8 +24,10 @@ license = "MIT OR Apache-2.0" authors = ["Manuel Cillero "] [workspace.dependencies] +argon2 = "0.5" async-trait = "0.1" axum = { version = "0.8" } +base64ct = { version = "1.8", features = ["alloc"] } change-detection = "1.2" chrono = "0.4" colored = "3.1" @@ -43,14 +48,18 @@ path-slash = "0.2" proc-macro2 = "1.0" proc-macro2-diagnostics = { version = "0.10", default-features = false } quote = "1.0" +rand_core = { version = "0.6", features = ["getrandom"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_qs = "1.1" +sha2 = "0.11" syn = { version = "2.0", features = ["full", "extra-traits"] } tempfile = "3.27" terminal_size = "0.4" +thiserror = "2.0" tokio = { version = "1", features = ["full"] } tower = { version = "0.5", features = ["util"] } -tower-http = { version = "0.6", features = ["fs"] } +tower-http = { version = "0.7", features = ["catch-panic", "fs"] } tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] } @@ -62,16 +71,19 @@ pagetop-macros = { version = "0.3", path = "helpers/pagetop-macros" } pagetop-minimal = { version = "0.1", path = "helpers/pagetop-minimal" } pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" } # Extensions -pagetop-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-htmx = { version = "0.1", path = "extensions/pagetop-htmx" } -pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" } +pagetop-htmx = { version = "0.1", path = "extensions/pagetop-htmx" } +pagetop-menu = { version = "0.1", path = "extensions/pagetop-menu" } +pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" } +pagetop-user = { version = "0.1", path = "extensions/pagetop-user" } # PageTop pagetop = { version = "0.5", path = "." } [workspace.dependencies.sea-orm] version = "1.1" -features = ["debug-print", "macros", "runtime-tokio-native-tls"] +features = ["debug-print", "macros", "runtime-tokio-native-tls", "with-chrono"] default-features = false [workspace.dependencies.sea-schema] @@ -99,6 +111,7 @@ default = [] testing = [] [dependencies] +async-trait.workspace = true axum.workspace = true chrono.workspace = true colored.workspace = true @@ -114,6 +127,7 @@ pagetop-minimal.workspace = true pagetop-statics.workspace = true serde.workspace = true terminal_size.workspace = true +thiserror.workspace = true tokio.workspace = true tower.workspace = true tower-http.workspace = true diff --git a/assets/css/basic.css b/assets/css/basic.css index 2c69d35c..5a4afbc0 100644 --- a/assets/css/basic.css +++ b/assets/css/basic.css @@ -44,21 +44,10 @@ body { -webkit-tap-highlight-color: transparent; } -/* - * Brand component - */ - -.brand { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--val-color--text); - font-size: 1.75rem; - font-weight: 600; - text-decoration: none; -} -.brand .image { - display: flex; +/* Header, Aside, Content y Footer se apilan en el mismo orden ocupando todo el ancho disponible. */ +.wrapper { + display: flex; + flex-direction: column; } /* @@ -81,11 +70,75 @@ body { color: #000; } .badge-primary { background-color: var(--val-color--primary); } -.badge-secondary { background-color: var(--val-color--secondary); } +.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-danger { background-color: var(--val-color--danger); } +.badge-severe { background-color: var(--val-color--danger); } + +/* + * Brand component + */ + +.brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--val-color--text); + font-size: 1.75rem; + font-weight: 600; + text-decoration: none; +} +.brand .image { + display: flex; +} + +/* + * Breadcrumb component + */ + +.breadcrumb { + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + font-size: 0.875rem; +} +.breadcrumb-item + .breadcrumb-item { + padding-left: 0.5rem; +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: 0.5rem; + color: var(--val-color--text--muted); + content: "/"; +} +.breadcrumb-item.active { + color: var(--val-color--text--muted); +} +.breadcrumb-item a { + color: var(--val-color--text); + text-decoration: none; +} +.breadcrumb-item a:hover { + text-decoration: underline; +} + +/* Geometr铆a base de .dropdown-button/.dropdown-toggle (ver "Dropdown component" m谩s abajo). Se + * declara aqu铆, antes de "Buttons", para que las clases .button-sm/.button-lg/.button-* ganen el + * empate de especificidad frente a ella cuando se combinan en el mismo elemento (CSS resuelve un + * empate por orden de aparici贸n en el fichero, no por el orden de las clases en el HTML). */ +.dropdown-button, +.dropdown-toggle { + padding: 0.5rem 0.75rem; + font: inherit; + color: var(--val-color--text); + background-color: transparent; + border: 1px solid var(--val-color--border); + border-radius: 0.375rem; + cursor: pointer; +} /* * Buttons @@ -101,6 +154,7 @@ body { 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; } @@ -116,10 +170,19 @@ body { 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-secondary, +.button-neutral, .button-success, -.button-danger { +.button-severe { color: #fff; } .button-info, @@ -127,40 +190,40 @@ body { color: #000; } .button-primary { background-color: var(--val-color--primary); border-color: var(--val-color--primary); } -.button-secondary { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); } +.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-danger { background-color: var(--val-color--danger); border-color: var(--val-color--danger); } +.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-secondary: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-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-danger: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-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-secondary, +.button-outline-neutral, .button-outline-success, .button-outline-info, .button-outline-warning, -.button-outline-danger { +.button-outline-severe { background-color: transparent; } .button-outline-primary { border-color: var(--val-color--primary); color: var(--val-color--primary); } -.button-outline-secondary { border-color: var(--val-color--secondary); color: var(--val-color--secondary); } +.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-danger { border-color: var(--val-color--danger); color: var(--val-color--danger); } +.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-secondary:hover { background-color: var(--val-color--secondary); border-color: var(--val-color--secondary); 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-danger:hover { background-color: var(--val-color--danger); border-color: var(--val-color--danger); color: #fff; } +.button-outline-severe:hover { background-color: var(--val-color--danger); border-color: var(--val-color--danger); color: #fff; } .button-link { font-weight: 400; @@ -184,6 +247,171 @@ body { gap: 0.5rem; } +/* + * Dialog component + */ + +.dialog { + padding: 0; + width: 100%; + max-width: 32rem; + color: var(--val-color--text); + background-color: var(--val-color--bg); + border: none; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); +} +.dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); +} + +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.25rem; + border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); +} + +.dialog-title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; +} + +.dialog-close { + width: 1.5rem; + height: 1.5rem; + padding: 0; + line-height: 1; + color: var(--val-color--text--muted); + background-color: transparent; + border: none; + cursor: pointer; +} +.dialog-close::before { + content: "\2715"; +} +.dialog-close:hover { + color: var(--val-color--text); +} + +.dialog-body { + padding: 1.25rem; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding: 1rem 1.25rem; + border-top: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); +} + +/* + * Dropdown component + */ + +.dropdown { + position: relative; + display: inline-block; +} + +/* La geometr铆a base de .dropdown-button/.dropdown-toggle vive junto a "Buttons" (antes de esta + * secci贸n), para que las clases .button-sm/.button-lg/.button-* ganen el empate de especificidad + * frente a ella (CSS resuelve un empate por orden de aparici贸n en el fichero, no por el orden de + * las clases en el HTML). */ +.dropdown-toggle::after { + content: "\25BC"; + display: inline-block; + margin-left: 0.35em; + font-size: 0.8em; + transition: transform .15s ease-in-out; +} +.dropdown:has(> .dropdown-menu.show) > .dropdown-toggle::after { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + z-index: 1000; + top: 100%; + left: 0; + min-width: 12rem; + margin: 0.25rem 0 0; + padding: 0.5rem 0; + list-style: none; + background-color: var(--val-color--bg); + border: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); + border-radius: 0.375rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + /* Closed by default */ + display: none; +} +.dropdown-menu.show { + display: block; +} + +.dropdown-item { + display: block; + width: 100%; + padding: 0.375rem 1rem; + text-align: left; + color: var(--val-color--text); + text-decoration: none; + background-color: transparent; + border: none; + font: inherit; + cursor: pointer; +} +.dropdown-item:hover { + background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent); +} +.dropdown-item.active { + color: var(--val-color--primary); + font-weight: 600; +} +.dropdown-item.disabled { + color: var(--val-color--text--muted); + pointer-events: none; +} + +.dropdown-item-text { + display: block; + padding: 0.375rem 1rem; + color: var(--val-color--text--muted); +} + +.dropdown-header { + display: block; + padding: 0.5rem 1rem 0.25rem; + margin: 0; + font-size: 0.8125rem; + font-weight: 600; + color: var(--val-color--text--muted); +} + +.dropdown-divider { + height: 0; + margin: 0.5rem 0; + border: none; + border-top: 1px solid var(--val-color--border); +} + +/* Utility: visually hidden but still accessible to assistive technology */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + /* * Form components */ @@ -283,175 +511,20 @@ input:disabled + label { color: var(--val-color--text--muted); } -/* - * Dialog component - */ - -.dialog { - padding: 0; - width: 100%; - max-width: 32rem; - color: var(--val-color--text); - background-color: var(--val-color--bg); - border: none; - border-radius: 0.5rem; - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); -} -.dialog::backdrop { - background-color: rgba(0, 0, 0, 0.5); -} - -.dialog-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem 1.25rem; - border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); -} - -.dialog-title { - margin: 0; - font-size: 1.125rem; - font-weight: 600; -} - -.dialog-close { - width: 1.5rem; - height: 1.5rem; - padding: 0; - line-height: 1; - color: var(--val-color--text--muted); - background-color: transparent; - border: none; - cursor: pointer; -} -.dialog-close::before { - content: "\2715"; -} -.dialog-close:hover { - color: var(--val-color--text); -} - -.dialog-body { - padding: 1.25rem; -} - -.dialog-footer { - display: flex; - justify-content: flex-end; - gap: 0.5rem; - padding: 1rem 1.25rem; - border-top: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); -} - -/* - * Dropdown component - */ - -.dropdown { - position: relative; - display: inline-block; -} - -.dropdown-button, -.dropdown-toggle { - padding: 0.5rem 0.75rem; - font: inherit; - color: var(--val-color--text); - background-color: transparent; +.form-field > select.form-select, +.form-field > input.form-control, +.form-field > textarea.form-control { border: 1px solid var(--val-color--border); border-radius: 0.375rem; - cursor: pointer; + background-color: #fff; + transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; } -.dropdown-toggle::after { - content: "\25BC"; - display: inline-block; - margin-left: 0.35em; - font-size: 0.8em; - transition: transform .15s ease-in-out; -} -.dropdown:has(> .dropdown-menu.show) > .dropdown-toggle::after { - transform: rotate(180deg); -} - -.dropdown-menu { - position: absolute; - z-index: 1000; - top: 100%; - left: 0; - min-width: 12rem; - margin: 0.25rem 0 0; - padding: 0.5rem 0; - list-style: none; - background-color: var(--val-color--bg); - border: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); - border-radius: 0.375rem; - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - /* Closed by default */ - display: none; -} -.dropdown-menu.show { - display: block; -} - -.dropdown-item { - display: block; - width: 100%; - padding: 0.375rem 1rem; - text-align: left; - color: var(--val-color--text); - text-decoration: none; - background-color: transparent; - border: none; - font: inherit; - cursor: pointer; -} -.dropdown-item:hover { - background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent); -} -.dropdown-item.active { - color: var(--val-color--primary); - font-weight: 600; -} -.dropdown-item.disabled { - color: var(--val-color--text--muted); - pointer-events: none; -} - -.dropdown-item-text { - display: block; - padding: 0.375rem 1rem; - color: var(--val-color--text--muted); -} - -.dropdown-header { - display: block; - padding: 0.5rem 1rem 0.25rem; - margin: 0; - font-size: 0.8125rem; - font-weight: 600; - color: var(--val-color--text--muted); -} - -.dropdown-divider { - height: 0; - margin: 0.5rem 0; - border: none; - border-top: 1px solid var(--val-color--border); -} - -/* Utility: visually hidden but still accessible to assistive technology */ -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; +.form-field > select.form-select:focus, +.form-field > input.form-control:focus, +.form-field > textarea.form-control:focus { + outline: none; + border-color: var(--val-color--primary); + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--val-color--primary) 25%, transparent); } /* @@ -504,59 +577,123 @@ input:disabled + label { } /* - * Table component + * Nav component */ -.table-responsive { - overflow-x: auto; - -webkit-overflow-scrolling: touch; +.nav { + display: flex; + align-items: center; + gap: 0.25rem; + list-style: none; + margin: 0; + padding: 0; } -.table { - width: 100%; - border-collapse: collapse; - text-align: left; +.nav-item { + position: relative; } -.table th, -.table td { +.nav-link { + display: block; padding: 0.5rem 0.75rem; - border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); + color: var(--val-color--text); + text-decoration: none; + border-radius: 0.375rem; } - -.table thead th { - border-bottom-width: 2px; +.nav-link:hover { + background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent); +} +.nav-link.active { + color: var(--val-color--primary); font-weight: 600; } - -.table-sort { - color: inherit; - text-decoration: none; -} -.table-sort:hover { - text-decoration: underline; -} -.table-sort::after { - content: "\25BC"; - margin-left: 0.25rem; - font-size: 0.8em; - opacity: 0.15; -} -.table-sort-asc::after { - content: "\25B2"; - font-size: 0.8em; - opacity: 1; -} -.table-sort-desc::after { - content: "\25BC"; - font-size: 0.8em; - opacity: 1; -} - -.table-empty { - text-align: center; - font-style: italic; +.nav-link.disabled { color: var(--val-color--text--muted); + pointer-events: none; +} + +.nav-start { + justify-content: flex-start; +} +.nav-center { + justify-content: center; +} +.nav-end { + justify-content: flex-end; +} +.nav-vertical { + flex-direction: column; + align-items: stretch; +} +.nav-fill > .nav-item { + flex: 1 1 auto; + text-align: center; +} +.nav-justified > .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +/* + * Navbar component + */ + +.navbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 1rem; +} + +.navbar-toggle { + display: none; + padding: 0.25rem 0.5rem; + font-size: 1.25rem; + line-height: 1; + color: inherit; + background-color: transparent; + border: 1px solid var(--val-color--border); + border-radius: 0.375rem; + cursor: pointer; +} +.navbar-toggle::before { + content: "\2630"; +} + +.navbar-content { + display: flex; + align-items: center; + gap: 1rem; + flex: 1 1 auto; +} + +.navbar-text { + color: var(--val-color--text--muted); +} + +@media (max-width: 767.98px) { + .navbar { + flex-wrap: wrap; + } + .navbar:has(.navbar-toggle) .navbar-toggle { + display: inline-block; + order: -1; + } + .navbar:has(.navbar-toggle) .navbar-content { + display: none; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .navbar:has(.navbar-toggle) .navbar-content.show { + display: flex; + } + .navbar .nav { + flex-direction: column; + align-items: stretch; + width: 100%; + } } /* @@ -676,7 +813,7 @@ input:disabled + label { border: 0; border-start-end-radius: 0; border-end-end-radius: 0; - background-color: var(--val-color--light); + background-color: var(--val-color--bg--muted); } .pager-jump-input > input.form-control { text-align: center; @@ -699,36 +836,60 @@ input:disabled + label { } /* - * Breadcrumb component + * Table component */ -.breadcrumb { - display: flex; - flex-wrap: wrap; - list-style: none; - margin: 0 0 1rem; - padding: 0.75rem 1rem; - font-size: 0.875rem; +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; } -.breadcrumb-item + .breadcrumb-item { - padding-left: 0.5rem; + +.table { + width: 100%; + border-collapse: collapse; + text-align: left; } -.breadcrumb-item + .breadcrumb-item::before { - float: left; - padding-right: 0.5rem; - color: var(--val-color--text--muted); - content: "/"; + +.table th, +.table td { + padding: 0.5rem 0.75rem; + border-bottom: 1px solid color-mix(in srgb, var(--val-color--border) 15%, var(--val-color--bg)); } -.breadcrumb-item.active { - color: var(--val-color--text--muted); + +.table thead th { + border-bottom-width: 2px; + font-weight: 600; } -.breadcrumb-item a { - color: var(--val-color--text); + +.table-sort { + color: inherit; text-decoration: none; } -.breadcrumb-item a:hover { +.table-sort:hover { text-decoration: underline; } +.table-sort::after { + content: "\25BC"; + margin-left: 0.25rem; + font-size: 0.8em; + opacity: 0.15; +} +.table-sort-asc::after { + content: "\25B2"; + font-size: 0.8em; + opacity: 1; +} +.table-sort-desc::after { + content: "\25BC"; + font-size: 0.8em; + opacity: 1; +} + +.table-empty { + text-align: center; + font-style: italic; + color: var(--val-color--text--muted); +} /* * Region Footer @@ -738,3 +899,76 @@ input:disabled + label { padding: .75rem 0 3rem; text-align: center; } + +/* + * pagetop-user: login page + */ + +.user-login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 70vh; + padding: 2rem 1rem; + background: radial-gradient( + ellipse 120% 60% at 50% -10%, + color-mix(in srgb, var(--val-color--primary) 18%, var(--val-color--bg)) 0%, + var(--val-color--bg) 70% + ); +} + +.user-login-card { + width: 100%; + max-width: 24rem; + padding: 2rem; + background-color: #fff; + border-radius: 0.75rem; + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.15); +} + +.user-login-card .form-label { + margin-top: 1.25rem; +} + +.user-login-card .button { + display: block; + width: 100%; + margin-top: 1.5rem; +} + +.user-admin-table-wrapper { + margin: 1.5rem 0; +} + +.user-admin-table { + font-size: 0.9375rem; +} + +.user-admin-permission-list { + list-style: none; + padding-left: 0; +} + +.user-admin-permission-granted { + font-weight: 600; +} + +.user-admin-permission-missing { + color: var(--val-color--text--muted); +} + +.user-form-error { + padding: 0.625rem 0.875rem; + margin-bottom: 1rem; + color: #fff; + background-color: var(--val-color--danger); + border-radius: 0.375rem; + font-size: 0.9375rem; +} + +.user-register-link, +.user-reset-link { + margin-top: 1rem; + text-align: center; + font-size: 0.9375rem; +} diff --git a/assets/css/intro.css b/assets/css/intro.css index bceacb68..5f07b378 100644 --- a/assets/css/intro.css +++ b/assets/css/intro.css @@ -24,7 +24,6 @@ body { .intro { position: relative; - min-width: 350px; color: var(--intro-color); background-color: var(--intro-bg-color); diff --git a/assets/js/basic.navbar.init.js b/assets/js/basic.navbar.init.js new file mode 100644 index 00000000..ee0a5e87 --- /dev/null +++ b/assets/js/basic.navbar.init.js @@ -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); + }); +}); diff --git a/build.rs b/build.rs index a83be1d4..3a7a6554 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -use pagetop_build::{StaticFilesBundle, copy_dir}; +use pagetop_build::{StaticFilesBundle, compile_scss, copy_dir, minify_js}; fn main() -> std::io::Result<()> { // Regenera `static/` desde cero s贸lo si hay cambios en `assets/`. @@ -7,6 +7,21 @@ fn main() -> std::io::Result<()> { copy_dir("assets", "static")?; + // CSS: genera la variante minificada de `basic.css` del tema Basic. + compile_scss("assets/css/basic.css", "static/css/basic.min.css")?; + + // JS: minifica el manejo de `Dialog` del tema Basic. + minify_js( + "assets/js/basic.dialog.init.js", + "static/js/basic.dialog.min.js", + )?; + + // JS: minifica la integraci贸n de `Dropdown` con `accessible-menu` del tema Basic. + minify_js( + "assets/js/basic.dropdown.init.js", + "static/js/basic.dropdown.min.js", + )?; + StaticFilesBundle::from_dir("./static", None) .with_name("assets") .build() diff --git a/examples/basic-nav.rs b/examples/basic-nav.rs new file mode 100644 index 00000000..8fc37f94 --- /dev/null +++ b/examples/basic-nav.rs @@ -0,0 +1,46 @@ +use pagetop::prelude::*; + +struct NavDemo; + +#[async_trait] +impl Extension for NavDemo { + fn dependencies(&self) -> Vec { + 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 +} diff --git a/examples/basic-navbar.rs b/examples/basic-navbar.rs new file mode 100644 index 00000000..ef4813aa --- /dev/null +++ b/examples/basic-navbar.rs @@ -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 { + 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 +} diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 6b064165..8273f207 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -142,18 +142,17 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child( button::ButtonSet::new() .with_button( - Button::submit(Lc::t("btn_submit", &LOC)).with_style( - button::ButtonStyle::Solid(Intent::Primary), - ), + Button::submit(Lc::t("btn_submit", &LOC)) + .with_style(button::Style::Solid(Intent::Primary)), ) .with_button( Button::reset(Lc::t("btn_reset", &LOC)).with_style( - button::ButtonStyle::Outline(Intent::Secondary), + button::Style::Outline(Intent::Neutral), ), ) .with_button( Button::plain(Lc::t("btn_cancel", &LOC)) - .with_style(button::ButtonStyle::Link), + .with_style(button::Style::Link), ), ), ), @@ -258,18 +257,17 @@ async fn form_controls(request: HttpRequest) -> Result { .with_child( button::ButtonSet::new() .with_button( - Button::submit(Lc::t("btn_submit", &LOC)).with_style( - button::ButtonStyle::Solid(Intent::Primary), - ), + Button::submit(Lc::t("btn_submit", &LOC)) + .with_style(button::Style::Solid(Intent::Primary)), ) .with_button( Button::reset(Lc::t("btn_reset", &LOC)).with_style( - button::ButtonStyle::Outline(Intent::Secondary), + button::Style::Outline(Intent::Neutral), ), ) .with_button( Button::plain(Lc::t("btn_cancel", &LOC)) - .with_style(button::ButtonStyle::Link), + .with_style(button::Style::Link), ), ), ), @@ -299,24 +297,22 @@ async fn form_controls(request: HttpRequest) -> Result { p { (Lc::t("dialog_delete_body", &LOC).using(cx)) } } })) - .with_footer( - Button::plain(Lc::t("btn_cancel", &LOC)) - .with_prop(PropsOp::set("data-dialog-dismiss", "modal")) - .with_style(button::ButtonStyle::Outline( - Intent::Secondary, - )), - ) .with_footer( Button::plain(Lc::t("btn_ok", &LOC)) .with_prop(PropsOp::set("data-dialog-dismiss", "modal")) - .with_style(button::ButtonStyle::Solid(Intent::Primary)), + .with_style(button::Style::Solid(Intent::Primary)), + ) + .with_footer( + Button::plain(Lc::t("btn_cancel", &LOC)) + .with_prop(PropsOp::set("data-dialog-dismiss", "modal")) + .with_style(button::Style::Outline(Intent::Neutral)), ), ) .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::ButtonStyle::Solid(Intent::Danger)), + .with_style(button::Style::Solid(Intent::Severe)), ), ), ) @@ -437,14 +433,14 @@ fn form_lists() -> Form { button::ButtonSet::new() .with_button( Button::submit(Lc::t("btn_submit", &LOC)) - .with_style(button::ButtonStyle::Solid(Intent::Primary)), + .with_style(button::Style::Solid(Intent::Primary)), ) .with_button( Button::reset(Lc::t("btn_reset", &LOC)) - .with_style(button::ButtonStyle::Outline(Intent::Secondary)), + .with_style(button::Style::Outline(Intent::Neutral)), ) .with_button( - Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::ButtonStyle::Link), + Button::plain(Lc::t("btn_cancel", &LOC)).with_style(button::Style::Link), ), ) } diff --git a/examples/locale/en-US/basic-navbar.ftl b/examples/locale/en-US/basic-navbar.ftl new file mode 100644 index 00000000..84fd12f3 --- /dev/null +++ b/examples/locale/en-US/basic-navbar.ftl @@ -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 diff --git a/examples/locale/es-ES/basic-navbar.ftl b/examples/locale/es-ES/basic-navbar.ftl new file mode 100644 index 00000000..bdb72660 --- /dev/null +++ b/examples/locale/es-ES/basic-navbar.ftl @@ -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 diff --git a/src/base/component.rs b/src/base/component.rs index 6523cfca..9fb10b77 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -47,6 +47,14 @@ pub use intro::{Intro, IntroOpening}; mod messages; pub use messages::Messages; +pub mod nav; +#[doc(inline)] +pub use nav::Nav; + +pub mod navbar; +#[doc(inline)] +pub use navbar::Navbar; + mod pager; pub use pager::{Pager, PagerAlign, PagerVisibility}; diff --git a/src/base/component/badge.rs b/src/base/component/badge.rs index a01a61c5..1a7c45a0 100644 --- a/src/base/component/badge.rs +++ b/src/base/component/badge.rs @@ -6,10 +6,10 @@ use crate::prelude::*; /// /// ```rust,no_run /// # use pagetop::prelude::*; -/// let badge = Badge::labeled(Lc::n("Admin")).with_intent(Intent::Danger); +/// let badge = Badge::labeled(Lc::n("Admin")).with_intent(Intent::Severe); /// /// // Equivalente usando el constructor directo del tipo. -/// let badge = Badge::danger(Lc::n("Admin")); +/// let badge = Badge::severe(Lc::n("Admin")); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Badge { @@ -19,7 +19,7 @@ pub struct Badge { label: Lc, /// Devuelve la intenci贸n sem谩ntica del badge. #[getters(copy)] - #[default(Intent::Secondary)] + #[default(Intent::Neutral)] intent: Intent, } @@ -33,10 +33,10 @@ impl Component for Badge { self.props.get_id() } - fn setup(&mut self, _cx: &Context) { + fn setup(&mut self, cx: &Context) { self.alter_prop(PropsOp::prepend_classes(util::join!( "badge badge-", - self.intent().as_str() + self.intent().color(cx) ))); } @@ -67,11 +67,11 @@ impl Badge { } } - /// Crea un badge de tipo *secondary* con la etiqueta indicada. - pub fn secondary(label: Lc) -> Self { + /// Crea un badge de tipo *neutral* con la etiqueta indicada. + pub fn neutral(label: Lc) -> Self { Self { label, - intent: Intent::Secondary, + intent: Intent::Neutral, ..Default::default() } } @@ -103,11 +103,11 @@ impl Badge { } } - /// Crea un badge de tipo *danger* con la etiqueta indicada. - pub fn danger(label: Lc) -> Self { + /// Crea un badge de tipo *severe* con la etiqueta indicada. + pub fn severe(label: Lc) -> Self { Self { label, - intent: Intent::Danger, + intent: Intent::Severe, ..Default::default() } } diff --git a/src/base/component/button.rs b/src/base/component/button.rs index fdfc22ea..4bbb1787 100644 --- a/src/base/component/button.rs +++ b/src/base/component/button.rs @@ -1,7 +1,7 @@ //! Definiciones para crear botones ([`Button`]) y conjuntos de botones ([`ButtonSet`]). mod props; -pub use props::{ButtonKind, ButtonStyle}; +pub use props::{Kind, Size, Style}; mod component; pub use component::Button; diff --git a/src/base/component/button/component.rs b/src/base/component/button/component.rs index aef92e7d..34a1bb1f 100644 --- a/src/base/component/button/component.rs +++ b/src/base/component/button/component.rs @@ -7,6 +7,12 @@ use crate::prelude::*; /// - [`Button::submit()`]: bot贸n de env铆o (por defecto). /// - [`Button::reset()`]: bot贸n de restablecimiento de valores. /// - [`Button::plain()`]: bot贸n gen茅rico sin comportamiento predeterminado. +/// - [`Button::anchor()`]: enlace de navegaci贸n real con el aspecto de un bot贸n. +/// +/// No confundir [`Button::anchor()`] con [`button::Style::Link`]: el primero renderiza un `` real que navega; el segundo es s贸lo un estilo visual (clase `button-link`) que se +/// aplica con [`with_style()`](Self::with_style) sobre cualquiera de las otras variantes, que +/// siguen siendo un `