diff --git a/.gitignore b/.gitignore index ed088133..06e70689 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Ignora directorios de compilación **/target +# Ignora directorios de archivos estáticos +**/static + # Archivos de log **/log/*.log* diff --git a/Cargo.lock b/Cargo.lock index 433ce60d..1ad66c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -933,7 +942,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.4", "bstr", "log", "regex-automata", @@ -965,6 +974,16 @@ dependencies = [ "rand", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", + "bumpalo", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1495,6 +1514,17 @@ dependencies = [ "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]] name = "miniz_oxide" version = "0.8.9" @@ -1721,7 +1751,6 @@ version = "0.1.0" dependencies = [ "pagetop", "pagetop-build", - "tokio", ] [[package]] @@ -1731,7 +1760,6 @@ dependencies = [ "pagetop", "pagetop-build", "serde", - "tokio", ] [[package]] @@ -1739,9 +1767,18 @@ name = "pagetop-build" version = "0.3.2" dependencies = [ "grass", + "minify-js", "pagetop-statics", ] +[[package]] +name = "pagetop-htmx" +version = "0.1.0" +dependencies = [ + "pagetop", + "pagetop-build", +] + [[package]] name = "pagetop-macros" version = "0.3.0" @@ -1812,6 +1849,19 @@ dependencies = [ "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 = "pastey" version = "0.2.2" @@ -2094,7 +2144,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.4", "memchr", "regex-syntax", ] diff --git a/Cargo.toml b/Cargo.toml index 269f752f..03a5944a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ # Extensions "extensions/pagetop-aliner", "extensions/pagetop-bootsier", + "extensions/pagetop-htmx", "extensions/pagetop-seaorm", ] @@ -35,6 +36,7 @@ indexmap = "2.14" indoc = "2.0" itoa = "1.0" mime_guess = "2.0" +minify-js = "0.6" parking_lot = "0.12" pastey = "0.2" path-slash = "0.2" @@ -62,6 +64,7 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" } # Extensions 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 pagetop = { version = "0.5", path = "." } diff --git a/README.md b/README.md index 89fade06..35023682 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- +

PageTop

diff --git a/static/banner.png b/assets/banner.png similarity index 100% rename from static/banner.png rename to assets/banner.png diff --git a/assets/css/basic.css b/assets/css/basic.css new file mode 100644 index 00000000..b35b29ef --- /dev/null +++ b/assets/css/basic.css @@ -0,0 +1,147 @@ +: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; + /* Colors */ + --val-color--bg: #fafafa; + --val-color--text: #212529; + --val-color--text--muted: color-mix(in srgb, var(--val-color--text) 50%, var(--val-color--bg)); + --val-color--border: #212529; + --val-color--primary: #0d6efd; + --val-color--danger: #dc3545; + --val-color--switch-off: #adb5bd; +} + +*, *::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; +} + +/* + * Form components + */ + +.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); +} + +/* + * Region Footer + */ + +.region-footer { + padding: .75rem 0 3rem; + text-align: center; +} diff --git a/static/css/intro.css b/assets/css/intro.css similarity index 94% rename from static/css/intro.css rename to assets/css/intro.css index 00fe0d21..cc60b7b8 100644 --- a/static/css/intro.css +++ b/assets/css/intro.css @@ -389,10 +389,10 @@ body { .intro-text-body .block { position: relative; } -.intro-text-body .block__title { +.intro-text-body .block-title { margin: 1em 0 .8em; } -.intro-text-body .block__title span { +.intro-text-body .block-title span { display: inline-block; padding: 10px 30px 14px; margin: 30px 20px 0; @@ -403,7 +403,7 @@ body { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.intro-text-body .block__title:before { +.intro-text-body .block-title:before { content: ""; height: 5px; position: absolute; @@ -416,7 +416,7 @@ body { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.intro-text-body .block__title:after { +.intro-text-body .block-title:after { content: ""; height: 120%; position: absolute; @@ -427,22 +427,22 @@ body { background: var(--intro-bg-block-1); transform: rotate(2deg); } -.intro-text-body .block:nth-of-type(6n+1) .block__title:after { +.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 { +.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 { +.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 { +.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 { +.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 { +.intro-text-body .block:nth-of-type(6n+6) .block-title:after { background: var(--intro-bg-block-6); } diff --git a/static/css/normalize.css b/assets/css/normalize.css similarity index 100% rename from static/css/normalize.css rename to assets/css/normalize.css diff --git a/static/favicon.ico b/assets/favicon.ico similarity index 100% rename from static/favicon.ico rename to assets/favicon.ico diff --git a/static/img/intro-header-sm.avif b/assets/img/intro-header-sm.avif similarity index 100% rename from static/img/intro-header-sm.avif rename to assets/img/intro-header-sm.avif diff --git a/static/img/intro-header-sm.jpg b/assets/img/intro-header-sm.jpg similarity index 100% rename from static/img/intro-header-sm.jpg rename to assets/img/intro-header-sm.jpg diff --git a/static/img/intro-header-sm.webp b/assets/img/intro-header-sm.webp similarity index 100% rename from static/img/intro-header-sm.webp rename to assets/img/intro-header-sm.webp diff --git a/static/img/intro-header.avif b/assets/img/intro-header.avif similarity index 100% rename from static/img/intro-header.avif rename to assets/img/intro-header.avif diff --git a/static/img/intro-header.jpg b/assets/img/intro-header.jpg similarity index 100% rename from static/img/intro-header.jpg rename to assets/img/intro-header.jpg diff --git a/static/img/intro-header.webp b/assets/img/intro-header.webp similarity index 100% rename from static/img/intro-header.webp rename to assets/img/intro-header.webp diff --git a/build.rs b/build.rs index 85e02e02..a83be1d4 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,12 @@ -use pagetop_build::StaticFilesBundle; +use pagetop_build::{StaticFilesBundle, copy_dir}; 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"); + + copy_dir("assets", "static")?; + StaticFilesBundle::from_dir("./static", None) .with_name("assets") .build() diff --git a/examples/form-controls.rs b/examples/form-controls.rs index 1c7f066e..172de633 100644 --- a/examples/form-controls.rs +++ b/examples/form-controls.rs @@ -1,5 +1,6 @@ use pagetop::prelude::*; +use pagetop_bootsier::theme::form; use pagetop_bootsier::theme::*; include_locales!(LOC from "examples/locale"); @@ -143,17 +144,20 @@ async fn form_controls(request: HttpRequest) -> Result { .with_value("form-selections"), ) // Botones de acción. + .with_child(Button::submit(L10n::t("btn_submit", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::solid( + Color::Primary, + )), + )) + .with_child(Button::reset(L10n::t("btn_reset", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::outline( + Color::Secondary, + )), + )) .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), + Button::plain(L10n::t("btn_cancel", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::link()), + ), ), ), ) @@ -175,6 +179,7 @@ async fn form_controls(request: HttpRequest) -> Result { .with_name("name") .with_label(L10n::t("label_name", &LOC)) .with_placeholder(L10n::t("placeholder_name", &LOC)) + .with_help_text(L10n::t("help_name", &LOC)) .with_required(true), ) .with_child( @@ -185,6 +190,7 @@ async fn form_controls(request: HttpRequest) -> Result { "placeholder_email", &LOC, )) + .with_help_text(L10n::t("help_email", &LOC)) .with_autocomplete( Some(form::Autocomplete::email()), ) @@ -262,194 +268,175 @@ async fn form_controls(request: HttpRequest) -> Result { .with_value("form-text"), ) // Botones de acción. + .with_child(Button::submit(L10n::t("btn_submit", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::solid( + Color::Primary, + )), + )) + .with_child(Button::reset(L10n::t("btn_reset", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::outline( + Color::Secondary, + )), + )) .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), + Button::plain(L10n::t("btn_cancel", &LOC)).with_prop( + PropsOp::add_classes(classes::ButtonColor::link()), + ), ), ), ) // Bloque 3: listas de selección y etiquetas flotantes. .with_child( Block::new() - .with_title(L10n::t("block_lists", &LOC)) - .with_child( - 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(L10n::t("fieldset_select", &LOC)) - .with_child( - form::select::Field::new() - .with_name("language") - .with_label(L10n::t("label_language", &LOC)) - .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_child( - form::Fieldset::new() - .with_legend(L10n::t("fieldset_floating", &LOC)) - .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_title( + if global::SETTINGS.app.theme.eq_ignore_ascii_case("bootsier") { + L10n::t("block_lists_floating", &LOC) + } else { + L10n::t("block_lists", &LOC) + }, + ) + .with_child(form_lists()), ), ) .render() } +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(L10n::t("fieldset_select", &LOC)) + .with_child( + form::select::Field::new() + .with_name("language") + .with_label(L10n::t("label_language", &LOC)) + .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: 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(L10n::t("fieldset_floating", &LOC)) + .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), + ), + ); + } + + form + // 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_prop(PropsOp::add_classes( + classes::ButtonColor::solid(Color::Primary), + )), + ) + .with_child( + Button::reset(L10n::t("btn_reset", &LOC)).with_prop(PropsOp::add_classes( + classes::ButtonColor::outline(Color::Secondary), + )), + ) + .with_child( + Button::plain(L10n::t("btn_cancel", &LOC)) + .with_prop(PropsOp::add_classes(classes::ButtonColor::link())), + ) +} + #[pagetop::main] async fn main() -> std::io::Result<()> { Application::prepare(&FormControls).run()?.await diff --git a/examples/locale/en-US/form-controls.ftl b/examples/locale/en-US/form-controls.ftl index 7c2b3c96..ce6c76ca 100644 --- a/examples/locale/en-US/form-controls.ftl +++ b/examples/locale/en-US/form-controls.ftl @@ -2,7 +2,8 @@ title = Form controls slogan = Bootsier form components showcase block_selections = Checkboxes, switches and radio buttons 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 fieldset_text = Text fields label_name = Full name @@ -16,6 +17,8 @@ label_url = Website placeholder_url = https://example.com label_search = Search 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 label_comment = Comment diff --git a/examples/locale/es-ES/form-controls.ftl b/examples/locale/es-ES/form-controls.ftl index 67a0fd2c..40781042 100644 --- a/examples/locale/es-ES/form-controls.ftl +++ b/examples/locale/es-ES/form-controls.ftl @@ -2,7 +2,8 @@ title = Controles de formulario slogan = Componentes Bootsier para formularios block_selections = Casillas, interruptores y botones de opción 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 fieldset_text = Campos de texto label_name = Nombre completo @@ -16,6 +17,8 @@ label_url = Sitio web placeholder_url = https://ejemplo.com label_search = 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 label_comment = Comentario diff --git a/examples/navbar-menus.rs b/examples/navbar-menus.rs index 7f8ccdda..06c4ed36 100644 --- a/examples/navbar-menus.rs +++ b/examples/navbar-menus.rs @@ -80,10 +80,9 @@ impl Extension for SuperMenu { )) .with_item(navbar::Item::nav( Nav::new() - .with_classes( - ClassesOp::Add, + .with_prop(PropsOp::add_classes( 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") })) diff --git a/extensions/pagetop-aliner/Cargo.toml b/extensions/pagetop-aliner/Cargo.toml index d2828b3f..072324e3 100644 --- a/extensions/pagetop-aliner/Cargo.toml +++ b/extensions/pagetop-aliner/Cargo.toml @@ -17,8 +17,5 @@ authors.workspace = true [dependencies] pagetop.workspace = true -[dev-dependencies] -tokio.workspace = true - [build-dependencies] pagetop-build.workspace = true diff --git a/extensions/pagetop-aliner/README.md b/extensions/pagetop-aliner/README.md index bf515d66..ded3aa6f 100644 --- a/extensions/pagetop-aliner/README.md +++ b/extensions/pagetop-aliner/README.md @@ -11,14 +11,13 @@
-## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## ⚡️ Guía rápida +## Guía rápida Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`: @@ -44,11 +43,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: @@ -67,10 +61,10 @@ use pagetop_aliner::Aliner; async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Aliner) - .add_child( + .with_child( Block::new() .with_title(L10n::l("sample_title")) - .add_child(Html::with(|cx| html! { + .with_child(Html::with(|cx| html! { p { (L10n::l("sample_content").using(cx)) } })), ) @@ -78,15 +72,13 @@ async fn homepage(request: HttpRequest) -> Result { } ``` - -## 🚧 Advertencia +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/extensions/pagetop-aliner/src/lib.rs b/extensions/pagetop-aliner/src/lib.rs index dedf4e19..33f32677 100644 --- a/extensions/pagetop-aliner/src/lib.rs +++ b/extensions/pagetop-aliner/src/lib.rs @@ -3,14 +3,13 @@

PageTop Aliner

-

Tema para PageTop que muestra esquemáticamente la composición de las páginas HTML.

+

Tema de PageTop que muestra esquemáticamente la composición de las páginas HTML.

[![Doc API](https://img.shields.io/docsrs/pagetop-aliner?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-aliner) [![Crates.io](https://img.shields.io/crates/v/pagetop-aliner.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-aliner) [![Descargas](https://img.shields.io/crates/d/pagetop-aliner.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-aliner) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia) -
## Sobre PageTop @@ -19,8 +18,7 @@ clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -# ⚡️ Guía rápida +## Guía rápida Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`: @@ -46,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: diff --git a/extensions/pagetop-bootsier/Cargo.toml b/extensions/pagetop-bootsier/Cargo.toml index 44b6d248..47766c52 100644 --- a/extensions/pagetop-bootsier/Cargo.toml +++ b/extensions/pagetop-bootsier/Cargo.toml @@ -18,8 +18,5 @@ authors.workspace = true pagetop.workspace = true serde.workspace = true -[dev-dependencies] -tokio.workspace = true - [build-dependencies] pagetop-build.workspace = true diff --git a/extensions/pagetop-bootsier/README.md b/extensions/pagetop-bootsier/README.md index f71f221e..69440ec6 100644 --- a/extensions/pagetop-bootsier/README.md +++ b/extensions/pagetop-bootsier/README.md @@ -11,14 +11,13 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## ⚡️ Guía rápida +## Guía rápida Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`: @@ -44,11 +43,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: @@ -67,10 +61,10 @@ use pagetop_bootsier::Bootsier; async fn homepage(request: HttpRequest) -> Result { Page::new(request) .with_theme(&Bootsier) - .add_child( + .with_child( Block::new() .with_title(L10n::l("sample_title")) - .add_child(Html::with(|cx| html! { + .with_child(Html::with(|cx| html! { p { (L10n::l("sample_content").using(cx)) } })), ) @@ -78,22 +72,19 @@ async fn homepage(request: HttpRequest) -> Result { } ``` - -## 📚 Créditos +## Créditos 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 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 ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/extensions/pagetop-bootsier/assets/_bootsier-custom.scss b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss new file mode 100644 index 00000000..51f08dd8 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss @@ -0,0 +1,84 @@ +// Bootsier CSS rules: self-hosted fonts, form components, and regions. + +// Self-hosted Source Sans 3 (SIL OFL 1.1), served from /bootsier/fonts. +// Required by AdminLTE 4, which declares it as the primary font family in $font-family-sans-serif. +@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; + } +} + +// 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; +} diff --git a/extensions/pagetop-bootsier/assets/_bootsier-icons.scss b/extensions/pagetop-bootsier/assets/_bootsier-icons.scss new file mode 100644 index 00000000..fb05eb1e --- /dev/null +++ b/extensions/pagetop-bootsier/assets/_bootsier-icons.scss @@ -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"; diff --git a/extensions/pagetop-bootsier/static/scss/_customs.scss b/extensions/pagetop-bootsier/assets/_bootsier-utilities.scss similarity index 64% rename from extensions/pagetop-bootsier/static/scss/_customs.scss rename to extensions/pagetop-bootsier/assets/_bootsier-utilities.scss index 6d0aa4e6..4192158e 100644 --- a/extensions/pagetop-bootsier/static/scss/_customs.scss +++ b/extensions/pagetop-bootsier/assets/_bootsier-utilities.scss @@ -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-cssgrid: true; -// Opacity -.bg-opacity-0 { - --bs-bg-opacity: 0; -} - -.border-opacity-0 { - --bs-border-opacity: 0; -} - -.text-opacity-0 { - --bs-text-opacity: 0; -} -.text-opacity-10 { - --bs-text-opacity: 0.1; -} - -// 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 +// Extend the $utilities map with additional classes. $utilities: map-merge( $utilities, ( - // Individual border widths + // Individual border widths per side. "border-top": ( property: border-top-width, class: border-top, @@ -95,7 +29,7 @@ $utilities: map-merge( class: border-start, values: $border-widths ), - // Individual rounded values + // Individual corner radii. "rounded-top-start": ( property: border-top-left-radius, class: rounded-top-start, @@ -156,11 +90,18 @@ $utilities: map-merge( 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; -} diff --git a/extensions/pagetop-bootsier/assets/_bootsier-variables.scss b/extensions/pagetop-bootsier/assets/_bootsier-variables.scss new file mode 100644 index 00000000..888e7450 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/_bootsier-variables.scss @@ -0,0 +1,4 @@ +// 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; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Italic.otf.woff2 b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Italic.otf.woff2 new file mode 100644 index 00000000..4a160ff1 Binary files /dev/null and b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Italic.otf.woff2 differ diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Upright.otf.woff2 b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Upright.otf.woff2 new file mode 100644 index 00000000..d1537d81 Binary files /dev/null and b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/fonts/SourceSans3VF-Upright.otf.woff2 differ diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js new file mode 100644 index 00000000..7ce13dff --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js @@ -0,0 +1,1081 @@ +/*! + * AdminLTE v4.0.0 (https://adminlte.io) + * Copyright 2014-2026 Colorlib + * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE) + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.adminlte = {})); +})(this, (function (exports) { 'use strict'; + + const domContentLoadedCallbacks = []; + const onDOMContentLoaded = (callback) => { + if (document.readyState === 'loading') { + if (!domContentLoadedCallbacks.length) { + document.addEventListener('DOMContentLoaded', () => { + for (const callback of domContentLoadedCallbacks) { + callback(); + } + }); + } + domContentLoadedCallbacks.push(callback); + } + else { + callback(); + } + }; + const slideUp = (target, duration = 500) => { + if (duration <= 1) { + target.style.display = 'none'; + return; + } + target.style.transitionProperty = 'height, margin, padding'; + target.style.transitionDuration = `${duration}ms`; + target.style.boxSizing = 'border-box'; + target.style.height = `${target.offsetHeight}px`; + target.style.overflow = 'hidden'; + globalThis.setTimeout(() => { + target.style.height = '0'; + target.style.paddingTop = '0'; + target.style.paddingBottom = '0'; + target.style.marginTop = '0'; + target.style.marginBottom = '0'; + }, 1); + globalThis.setTimeout(() => { + target.style.display = 'none'; + target.style.removeProperty('height'); + target.style.removeProperty('padding-top'); + target.style.removeProperty('padding-bottom'); + target.style.removeProperty('margin-top'); + target.style.removeProperty('margin-bottom'); + target.style.removeProperty('overflow'); + target.style.removeProperty('transition-duration'); + target.style.removeProperty('transition-property'); + }, duration); + }; + const slideDown = (target, duration = 500) => { + target.style.removeProperty('display'); + let { display } = globalThis.getComputedStyle(target); + if (display === 'none') { + display = 'block'; + } + target.style.display = display; + if (duration <= 1) { + return; + } + const height = target.offsetHeight; + target.style.overflow = 'hidden'; + target.style.height = '0'; + target.style.paddingTop = '0'; + target.style.paddingBottom = '0'; + target.style.marginTop = '0'; + target.style.marginBottom = '0'; + globalThis.setTimeout(() => { + target.style.boxSizing = 'border-box'; + target.style.transitionProperty = 'height, margin, padding'; + target.style.transitionDuration = `${duration}ms`; + target.style.height = `${height}px`; + target.style.removeProperty('padding-top'); + target.style.removeProperty('padding-bottom'); + target.style.removeProperty('margin-top'); + target.style.removeProperty('margin-bottom'); + }, 1); + globalThis.setTimeout(() => { + target.style.removeProperty('height'); + target.style.removeProperty('overflow'); + target.style.removeProperty('transition-duration'); + target.style.removeProperty('transition-property'); + }, duration); + }; + + const CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition'; + const CLASS_NAME_APP_LOADED = 'app-loaded'; + class Layout { + _element; + _holdTransitionTimer; + constructor(element) { + this._element = element; + this._holdTransitionTimer = undefined; + } + holdTransition(time = 100) { + if (this._holdTransitionTimer) { + clearTimeout(this._holdTransitionTimer); + } + document.body.classList.add(CLASS_NAME_HOLD_TRANSITIONS); + this._holdTransitionTimer = setTimeout(() => { + document.body.classList.remove(CLASS_NAME_HOLD_TRANSITIONS); + }, time); + } + } + onDOMContentLoaded(() => { + const layout = new Layout(document.body); + window.addEventListener('resize', () => layout.holdTransition(200)); + setTimeout(() => { + document.body.classList.add(CLASS_NAME_APP_LOADED); + }, 400); + }); + + const DATA_KEY$4 = 'lte.card-widget'; + const EVENT_KEY$4 = `.${DATA_KEY$4}`; + const EVENT_COLLAPSED$2 = `collapsed${EVENT_KEY$4}`; + const EVENT_EXPANDED$2 = `expanded${EVENT_KEY$4}`; + const EVENT_REMOVE = `remove${EVENT_KEY$4}`; + const EVENT_MAXIMIZED$1 = `maximized${EVENT_KEY$4}`; + const EVENT_MINIMIZED$1 = `minimized${EVENT_KEY$4}`; + const CLASS_NAME_CARD = 'card'; + const CLASS_NAME_COLLAPSED = 'collapsed-card'; + const CLASS_NAME_COLLAPSING = 'collapsing-card'; + const CLASS_NAME_EXPANDING = 'expanding-card'; + const CLASS_NAME_WAS_COLLAPSED = 'was-collapsed'; + const CLASS_NAME_MAXIMIZED = 'maximized-card'; + const SELECTOR_DATA_REMOVE = '[data-lte-toggle="card-remove"]'; + const SELECTOR_DATA_COLLAPSE = '[data-lte-toggle="card-collapse"]'; + const SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle="card-maximize"]'; + const SELECTOR_CARD = `.${CLASS_NAME_CARD}`; + const SELECTOR_CARD_BODY = '.card-body'; + const SELECTOR_CARD_FOOTER = '.card-footer'; + const Default$1 = { + animationSpeed: 500, + collapseTrigger: SELECTOR_DATA_COLLAPSE, + removeTrigger: SELECTOR_DATA_REMOVE, + maximizeTrigger: SELECTOR_DATA_MAXIMIZE + }; + class CardWidget { + _element; + _parent; + _clone; + _config; + constructor(element, config) { + this._element = element; + this._parent = element.closest(SELECTOR_CARD); + if (element.classList.contains(CLASS_NAME_CARD)) { + this._parent = element; + } + this._config = { ...Default$1, ...config }; + } + collapse() { + const event = new Event(EVENT_COLLAPSED$2); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_COLLAPSING); + const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`); + elm.forEach(el => { + if (el instanceof HTMLElement) { + slideUp(el, this._config.animationSpeed); + } + }); + setTimeout(() => { + if (this._parent) { + this._parent.classList.add(CLASS_NAME_COLLAPSED); + this._parent.classList.remove(CLASS_NAME_COLLAPSING); + } + }, this._config.animationSpeed); + } + this._element?.dispatchEvent(event); + } + expand() { + const event = new Event(EVENT_EXPANDED$2); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_EXPANDING); + const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`); + elm.forEach(el => { + if (el instanceof HTMLElement) { + slideDown(el, this._config.animationSpeed); + } + }); + setTimeout(() => { + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_COLLAPSED, CLASS_NAME_EXPANDING); + } + }, this._config.animationSpeed); + } + this._element?.dispatchEvent(event); + } + remove() { + const event = new Event(EVENT_REMOVE); + if (this._parent) { + slideUp(this._parent, this._config.animationSpeed); + } + this._element?.dispatchEvent(event); + } + toggle() { + if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) { + this.expand(); + return; + } + this.collapse(); + } + maximize() { + const event = new Event(EVENT_MAXIMIZED$1); + if (this._parent) { + this._parent.style.height = `${this._parent.offsetHeight}px`; + this._parent.style.width = `${this._parent.offsetWidth}px`; + this._parent.style.transition = 'all .15s'; + setTimeout(() => { + const htmlTag = document.querySelector('html'); + if (htmlTag) { + htmlTag.classList.add(CLASS_NAME_MAXIMIZED); + } + if (this._parent) { + this._parent.classList.add(CLASS_NAME_MAXIMIZED); + if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) { + this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED); + } + } + }, 150); + } + this._element?.dispatchEvent(event); + } + minimize() { + const event = new Event(EVENT_MINIMIZED$1); + if (this._parent) { + this._parent.style.height = 'auto'; + this._parent.style.width = 'auto'; + this._parent.style.transition = 'all .15s'; + setTimeout(() => { + const htmlTag = document.querySelector('html'); + if (htmlTag) { + htmlTag.classList.remove(CLASS_NAME_MAXIMIZED); + } + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_MAXIMIZED); + if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) { + this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED); + } + } + }, 10); + } + this._element?.dispatchEvent(event); + } + toggleMaximize() { + if (this._parent?.classList.contains(CLASS_NAME_MAXIMIZED)) { + this.minimize(); + return; + } + this.maximize(); + } + } + onDOMContentLoaded(() => { + const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE); + collapseBtn.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + const target = event.target; + const data = new CardWidget(target, Default$1); + data.toggle(); + }); + }); + const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE); + removeBtn.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + const target = event.target; + const data = new CardWidget(target, Default$1); + data.remove(); + }); + }); + const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE); + maxBtn.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + const target = event.target; + const data = new CardWidget(target, Default$1); + data.toggleMaximize(); + }); + }); + }); + + const DATA_KEY$3 = 'lte.treeview'; + const EVENT_KEY$3 = `.${DATA_KEY$3}`; + const EVENT_EXPANDED$1 = `expanded${EVENT_KEY$3}`; + const EVENT_COLLAPSED$1 = `collapsed${EVENT_KEY$3}`; + const EVENT_LOAD_DATA_API = `load${EVENT_KEY$3}`; + const CLASS_NAME_MENU_OPEN = 'menu-open'; + const SELECTOR_NAV_ITEM = '.nav-item'; + const SELECTOR_NAV_LINK = '.nav-link'; + const SELECTOR_TREEVIEW_MENU = '.nav-treeview'; + const SELECTOR_DATA_TOGGLE$1 = '[data-lte-toggle="treeview"]'; + const Default = { + animationSpeed: 300, + accordion: true + }; + class Treeview { + _element; + _config; + constructor(element, config) { + this._element = element; + this._config = { ...Default, ...config }; + } + open() { + const event = new Event(EVENT_EXPANDED$1); + if (this._config.accordion) { + const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`); + openMenuList?.forEach(openMenu => { + if (openMenu !== this._element.parentElement) { + openMenu.classList.remove(CLASS_NAME_MENU_OPEN); + const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU); + if (childElement) { + slideUp(childElement, this._config.animationSpeed); + } + } + }); + } + this._element.classList.add(CLASS_NAME_MENU_OPEN); + const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU); + if (childElement) { + slideDown(childElement, this._config.animationSpeed); + } + this._element.dispatchEvent(event); + } + close() { + const event = new Event(EVENT_COLLAPSED$1); + this._element.classList.remove(CLASS_NAME_MENU_OPEN); + const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU); + if (childElement) { + slideUp(childElement, this._config.animationSpeed); + } + this._element.dispatchEvent(event); + } + toggle() { + if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) { + this.close(); + } + else { + this.open(); + } + } + } + onDOMContentLoaded(() => { + const openMenuItems = document.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`); + openMenuItems.forEach(menuItem => { + const childElement = menuItem.querySelector(SELECTOR_TREEVIEW_MENU); + if (childElement) { + slideDown(childElement, 0); + const event = new Event(EVENT_LOAD_DATA_API); + menuItem.dispatchEvent(event); + } + }); + const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE$1); + button.forEach(btn => { + btn.addEventListener('click', event => { + const target = event.target; + const targetItem = target.closest(SELECTOR_NAV_ITEM); + const targetLink = target.closest(SELECTOR_NAV_LINK); + const targetTreeviewMenu = targetItem?.querySelector(SELECTOR_TREEVIEW_MENU); + const lteToggleElement = event.currentTarget; + if (!targetTreeviewMenu) { + return; + } + if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') { + event.preventDefault(); + } + if (targetItem) { + const accordionAttr = lteToggleElement.dataset.accordion; + const animationSpeedAttr = lteToggleElement.dataset.animationSpeed; + const config = { + accordion: accordionAttr === undefined ? Default.accordion : accordionAttr === 'true', + animationSpeed: animationSpeedAttr === undefined ? Default.animationSpeed : Number(animationSpeedAttr) + }; + const data = new Treeview(targetItem, config); + data.toggle(); + } + }); + }); + }); + + const DATA_KEY$2 = 'lte.direct-chat'; + const EVENT_KEY$2 = `.${DATA_KEY$2}`; + const EVENT_EXPANDED = `expanded${EVENT_KEY$2}`; + const EVENT_COLLAPSED = `collapsed${EVENT_KEY$2}`; + const SELECTOR_DATA_TOGGLE = '[data-lte-toggle="chat-pane"]'; + const SELECTOR_DIRECT_CHAT = '.direct-chat'; + const CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'; + class DirectChat { + _element; + constructor(element) { + this._element = element; + } + toggle() { + if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) { + const event = new Event(EVENT_COLLAPSED); + this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN); + this._element.dispatchEvent(event); + } + else { + const event = new Event(EVENT_EXPANDED); + this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN); + this._element.dispatchEvent(event); + } + } + } + onDOMContentLoaded(() => { + const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE); + button.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + const target = event.target; + const chatPane = target.closest(SELECTOR_DIRECT_CHAT); + if (chatPane) { + const data = new DirectChat(chatPane); + data.toggle(); + } + }); + }); + }); + + const DATA_KEY$1 = 'lte.fullscreen'; + const EVENT_KEY$1 = `.${DATA_KEY$1}`; + const EVENT_MAXIMIZED = `maximized${EVENT_KEY$1}`; + const EVENT_MINIMIZED = `minimized${EVENT_KEY$1}`; + const SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle="fullscreen"]'; + const SELECTOR_MAXIMIZE_ICON = '[data-lte-icon="maximize"]'; + const SELECTOR_MINIMIZE_ICON = '[data-lte-icon="minimize"]'; + class FullScreen { + _element; + _config; + constructor(element, config) { + this._element = element; + this._config = config; + } + inFullScreen() { + const event = new Event(EVENT_MAXIMIZED); + const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON); + const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON); + void document.documentElement.requestFullscreen(); + if (iconMaximize) { + iconMaximize.classList.add('d-none'); + } + if (iconMinimize) { + iconMinimize.classList.remove('d-none'); + } + this._element.dispatchEvent(event); + } + outFullscreen() { + const event = new Event(EVENT_MINIMIZED); + const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON); + const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON); + void document.exitFullscreen(); + if (iconMaximize) { + iconMaximize.classList.remove('d-none'); + } + if (iconMinimize) { + iconMinimize.classList.add('d-none'); + } + this._element.dispatchEvent(event); + } + toggleFullScreen() { + if (document.fullscreenEnabled) { + if (document.fullscreenElement) { + this.outFullscreen(); + } + else { + this.inFullScreen(); + } + } + } + } + onDOMContentLoaded(() => { + const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE); + buttons.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + const target = event.target; + const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE); + if (button) { + const data = new FullScreen(button, undefined); + data.toggleFullScreen(); + } + }); + }); + }); + + const DATA_KEY = 'lte.push-menu'; + const EVENT_KEY = `.${DATA_KEY}`; + const EVENT_OPEN = `open${EVENT_KEY}`; + const EVENT_COLLAPSE = `collapse${EVENT_KEY}`; + const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini'; + const CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand'; + const CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay'; + const CLASS_NAME_SIDEBAR_COLLAPSE = 'sidebar-collapse'; + const CLASS_NAME_SIDEBAR_OPEN = 'sidebar-open'; + const SELECTOR_APP_SIDEBAR = '.app-sidebar'; + const SELECTOR_APP_WRAPPER = '.app-wrapper'; + const SELECTOR_SIDEBAR_EXPAND = `[class*="${CLASS_NAME_SIDEBAR_EXPAND}"]`; + const SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle="sidebar"]'; + const STORAGE_KEY_SIDEBAR_STATE = 'lte.sidebar.state'; + const Defaults = { + sidebarBreakpoint: 992, + enablePersistence: false + }; + class PushMenu { + _element; + _config; + constructor(element, config) { + this._element = element; + this._config = { ...Defaults, ...config }; + } + isCollapsed() { + return document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE); + } + isExplicitlyOpen() { + return document.body.classList.contains(CLASS_NAME_SIDEBAR_OPEN); + } + isMiniMode() { + return document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI); + } + isMobileSize() { + return globalThis.innerWidth <= this._config.sidebarBreakpoint; + } + expand() { + document.body.classList.remove(CLASS_NAME_SIDEBAR_COLLAPSE); + if (this.isMobileSize()) { + document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN); + } + this._element.dispatchEvent(new Event(EVENT_OPEN)); + } + collapse() { + document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN); + document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE); + this._element.dispatchEvent(new Event(EVENT_COLLAPSE)); + } + toggle() { + const isCollapsed = this.isCollapsed(); + if (isCollapsed) { + this.expand(); + } + else { + this.collapse(); + } + if (this._config.enablePersistence) { + this.saveSidebarState(isCollapsed ? CLASS_NAME_SIDEBAR_OPEN : CLASS_NAME_SIDEBAR_COLLAPSE); + } + } + setupSidebarBreakPoint() { + const sidebarExpand = document.querySelector(SELECTOR_SIDEBAR_EXPAND); + if (!sidebarExpand) { + return; + } + const content = globalThis.getComputedStyle(sidebarExpand, '::before') + .getPropertyValue('content'); + if (!content || content === 'none') { + return; + } + const breakpointValue = Number(content.replace(/[^\d.-]/g, '')); + if (Number.isNaN(breakpointValue)) { + return; + } + this._config = { ...this._config, sidebarBreakpoint: breakpointValue }; + } + updateStateByResponsiveLogic() { + if (this.isMobileSize()) { + if (!this.isExplicitlyOpen()) { + this.collapse(); + } + } + else { + if (!(this.isMiniMode() && this.isCollapsed())) { + this.expand(); + } + } + } + saveSidebarState(state) { + if (globalThis.localStorage === undefined) { + return; + } + try { + localStorage.setItem(STORAGE_KEY_SIDEBAR_STATE, state); + } + catch { + } + } + loadSidebarState() { + if (globalThis.localStorage === undefined) { + return; + } + try { + const storedState = localStorage.getItem(STORAGE_KEY_SIDEBAR_STATE); + if (storedState === CLASS_NAME_SIDEBAR_COLLAPSE) { + this.collapse(); + } + else if (storedState === CLASS_NAME_SIDEBAR_OPEN) { + this.expand(); + } + else { + this.updateStateByResponsiveLogic(); + } + } + catch { + this.updateStateByResponsiveLogic(); + } + } + clearSidebarState() { + if (globalThis.localStorage === undefined) { + return; + } + try { + localStorage.removeItem(STORAGE_KEY_SIDEBAR_STATE); + } + catch { + } + } + init() { + this.setupSidebarBreakPoint(); + if (!this._config.enablePersistence) { + this.clearSidebarState(); + } + if (this._config.enablePersistence && !this.isMobileSize()) { + this.loadSidebarState(); + } + else { + this.updateStateByResponsiveLogic(); + } + } + } + onDOMContentLoaded(() => { + const sidebar = document?.querySelector(SELECTOR_APP_SIDEBAR); + if (!sidebar) { + return; + } + const sidebarBreakpointAttr = sidebar.dataset.sidebarBreakpoint; + const enablePersistenceAttr = sidebar.dataset.enablePersistence; + const config = { + sidebarBreakpoint: sidebarBreakpointAttr === undefined ? + Defaults.sidebarBreakpoint : + Number(sidebarBreakpointAttr), + enablePersistence: enablePersistenceAttr === undefined ? + Defaults.enablePersistence : + enablePersistenceAttr === 'true' + }; + const pushMenu = new PushMenu(sidebar, config); + pushMenu.init(); + window.addEventListener('resize', () => { + pushMenu.setupSidebarBreakPoint(); + pushMenu.updateStateByResponsiveLogic(); + }); + const sidebarOverlay = document.createElement('div'); + sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY; + document.querySelector(SELECTOR_APP_WRAPPER)?.append(sidebarOverlay); + let overlayTouchMoved = false; + sidebarOverlay.addEventListener('touchstart', () => { + overlayTouchMoved = false; + }, { passive: true }); + sidebarOverlay.addEventListener('touchmove', () => { + overlayTouchMoved = true; + }, { passive: true }); + sidebarOverlay.addEventListener('touchend', event => { + if (!overlayTouchMoved) { + event.preventDefault(); + pushMenu.collapse(); + } + overlayTouchMoved = false; + }, { passive: false }); + sidebarOverlay.addEventListener('click', event => { + event.preventDefault(); + pushMenu.collapse(); + }); + const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE); + fullBtn.forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + let button = event.currentTarget; + if (button?.dataset.lteToggle !== 'sidebar') { + button = button?.closest(SELECTOR_SIDEBAR_TOGGLE); + } + if (button) { + event?.preventDefault(); + pushMenu.toggle(); + } + }); + }); + }); + + class AccessibilityManager { + config; + liveRegion = null; + focusHistory = []; + constructor(config = {}) { + this.config = { + announcements: true, + skipLinks: true, + focusManagement: true, + keyboardNavigation: true, + reducedMotion: true, + ...config + }; + this.init(); + } + init() { + if (this.config.announcements) { + this.createLiveRegion(); + } + if (this.config.skipLinks) { + this.addSkipLinks(); + } + if (this.config.focusManagement) { + this.initFocusManagement(); + } + if (this.config.keyboardNavigation) { + this.initKeyboardNavigation(); + } + if (this.config.reducedMotion) { + this.respectReducedMotion(); + } + this.initErrorAnnouncements(); + this.initTableAccessibility(); + this.initFormAccessibility(); + } + createLiveRegion() { + if (this.liveRegion) + return; + this.liveRegion = document.createElement('div'); + this.liveRegion.id = 'live-region'; + this.liveRegion.className = 'live-region'; + this.liveRegion.setAttribute('aria-live', 'polite'); + this.liveRegion.setAttribute('aria-atomic', 'true'); + this.liveRegion.setAttribute('role', 'status'); + document.body.append(this.liveRegion); + } + addSkipLinks() { + const skipLinksContainer = document.createElement('div'); + skipLinksContainer.className = 'skip-links'; + const skipToMain = document.createElement('a'); + skipToMain.href = '#main'; + skipToMain.className = 'skip-link'; + skipToMain.textContent = 'Skip to main content'; + const skipToNav = document.createElement('a'); + skipToNav.href = '#navigation'; + skipToNav.className = 'skip-link'; + skipToNav.textContent = 'Skip to navigation'; + skipLinksContainer.append(skipToMain); + skipLinksContainer.append(skipToNav); + document.body.insertBefore(skipLinksContainer, document.body.firstChild); + this.ensureSkipTargets(); + } + ensureSkipTargets() { + const main = document.querySelector('#main, main, [role="main"]'); + if (main && !main.id) { + main.id = 'main'; + } + if (main && !main.hasAttribute('tabindex')) { + main.setAttribute('tabindex', '-1'); + } + const nav = document.querySelector('#navigation, nav, [role="navigation"]'); + if (nav && !nav.id) { + nav.id = 'navigation'; + } + if (nav && !nav.hasAttribute('tabindex')) { + nav.setAttribute('tabindex', '-1'); + } + } + initFocusManagement() { + document.addEventListener('keydown', (event) => { + if (event.key === 'Tab') { + this.handleTabNavigation(event); + } + if (event.key === 'Escape') { + this.handleEscapeKey(event); + } + }); + this.initModalFocusManagement(); + this.initDropdownFocusManagement(); + } + handleTabNavigation(event) { + const focusableElements = this.getFocusableElements(); + const currentIndex = focusableElements.indexOf(document.activeElement); + if (event.shiftKey) { + if (currentIndex <= 0) { + event.preventDefault(); + focusableElements.at(-1)?.focus(); + } + } + else if (currentIndex >= focusableElements.length - 1) { + event.preventDefault(); + focusableElements[0]?.focus(); + } + } + getFocusableElements() { + const selector = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])', + '[contenteditable="true"]' + ].join(', '); + return Array.from(document.querySelectorAll(selector)); + } + handleEscapeKey(event) { + const activeModal = document.querySelector('.modal.show'); + if (activeModal) { + return; + } + const activeDropdown = document.querySelector('.dropdown-menu.show'); + if (activeDropdown) { + const toggleButton = document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]'); + toggleButton?.click(); + event.preventDefault(); + } + } + initKeyboardNavigation() { + document.addEventListener('keydown', (event) => { + const target = event.target; + if (target.closest('.nav, .navbar-nav, .dropdown-menu')) { + this.handleMenuNavigation(event); + } + if ((event.key === 'Enter' || event.key === ' ') && target.hasAttribute('role') && target.getAttribute('role') === 'button' && !target.matches('button, input[type="button"], input[type="submit"]')) { + event.preventDefault(); + target.click(); + } + }); + } + handleMenuNavigation(event) { + if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) { + return; + } + const currentElement = event.target; + const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []); + const currentIndex = menuItems.indexOf(currentElement); + let nextIndex; + switch (event.key) { + case 'ArrowDown': + case 'ArrowRight': { + nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0; + break; + } + case 'ArrowUp': + case 'ArrowLeft': { + nextIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1; + break; + } + case 'Home': { + nextIndex = 0; + break; + } + case 'End': { + nextIndex = menuItems.length - 1; + break; + } + default: { + return; + } + } + event.preventDefault(); + menuItems[nextIndex]?.focus(); + } + respectReducedMotion() { + const prefersReducedMotion = globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) { + document.body.classList.add('reduce-motion'); + document.documentElement.style.scrollBehavior = 'auto'; + const style = document.createElement('style'); + style.textContent = ` + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + `; + document.head.append(style); + } + } + initErrorAnnouncements() { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node; + if (element.matches('.alert-danger, .invalid-feedback, .error')) { + this.announce(element.textContent || 'Error occurred', 'assertive'); + } + if (element.matches('.alert-success, .success')) { + this.announce(element.textContent || 'Success', 'polite'); + } + } + }); + }); + }); + observer.observe(document.body, { + childList: true, + subtree: true + }); + } + initTableAccessibility() { + document.querySelectorAll('table').forEach((table) => { + if (!table.hasAttribute('role')) { + table.setAttribute('role', 'table'); + } + table.querySelectorAll('th').forEach((th) => { + if (!th.hasAttribute('scope')) { + const isInThead = th.closest('thead'); + const isFirstColumn = th.cellIndex === 0; + if (isInThead) { + th.setAttribute('scope', 'col'); + } + else if (isFirstColumn) { + th.setAttribute('scope', 'row'); + } + } + }); + if (!table.querySelector('caption') && table.hasAttribute('title')) { + const caption = document.createElement('caption'); + caption.textContent = table.getAttribute('title') || ''; + table.insertBefore(caption, table.firstChild); + } + }); + } + initFormAccessibility() { + document.querySelectorAll('input, select, textarea').forEach((input) => { + const htmlInput = input; + if (!htmlInput.labels?.length && !htmlInput.hasAttribute('aria-label') && !htmlInput.hasAttribute('aria-labelledby')) { + const placeholder = htmlInput.getAttribute('placeholder'); + if (placeholder) { + htmlInput.setAttribute('aria-label', placeholder); + } + } + if (htmlInput.hasAttribute('required')) { + const label = htmlInput.labels?.[0]; + if (label && !label.querySelector('.required-indicator')) { + const indicator = document.createElement('span'); + indicator.className = 'required-indicator sr-only'; + indicator.textContent = ' (required)'; + label.append(indicator); + } + } + if (!htmlInput.classList.contains('disable-adminlte-validations')) { + htmlInput.addEventListener('invalid', () => { + this.handleFormError(htmlInput); + }); + } + }); + } + handleFormError(input) { + const errorId = `${input.id || input.name}-error`; + let errorElement = document.getElementById(errorId); + if (!errorElement) { + errorElement = document.createElement('div'); + errorElement.id = errorId; + errorElement.className = 'invalid-feedback'; + errorElement.setAttribute('role', 'alert'); + input.parentNode?.append(errorElement); + } + errorElement.textContent = input.validationMessage; + input.setAttribute('aria-describedby', errorId); + input.classList.add('is-invalid'); + this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive'); + } + initModalFocusManagement() { + document.addEventListener('shown.bs.modal', (event) => { + const modal = event.target; + const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (focusableElements.length > 0) { + focusableElements[0].focus(); + } + this.focusHistory.push(document.activeElement); + }); + document.addEventListener('hidden.bs.modal', () => { + const previousElement = this.focusHistory.pop(); + if (previousElement) { + previousElement.focus(); + } + }); + } + initDropdownFocusManagement() { + document.addEventListener('shown.bs.dropdown', (event) => { + const dropdown = event.target; + const menu = dropdown.querySelector('.dropdown-menu'); + const firstItem = menu?.querySelector('a, button'); + if (firstItem) { + firstItem.focus(); + } + }); + } + announce(message, priority = 'polite') { + if (!this.liveRegion) { + this.createLiveRegion(); + } + if (this.liveRegion) { + this.liveRegion.setAttribute('aria-live', priority); + this.liveRegion.textContent = message; + setTimeout(() => { + if (this.liveRegion) { + this.liveRegion.textContent = ''; + } + }, 1000); + } + } + focusElement(selector) { + const element = document.querySelector(selector); + if (element) { + element.focus(); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + trapFocus(container) { + const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + const focusableArray = Array.from(focusableElements); + const firstElement = focusableArray[0]; + const lastElement = focusableArray.at(-1); + container.addEventListener('keydown', (event) => { + if (event.key === 'Tab') { + if (event.shiftKey) { + if (document.activeElement === firstElement) { + lastElement?.focus(); + event.preventDefault(); + } + } + else if (document.activeElement === lastElement) { + firstElement.focus(); + event.preventDefault(); + } + } + }); + } + addLandmarks() { + const main = document.querySelector('main'); + if (!main) { + const appMain = document.querySelector('.app-main'); + if (appMain) { + appMain.setAttribute('role', 'main'); + appMain.id = 'main'; + } + } + document.querySelectorAll('.navbar-nav, .nav').forEach((nav, index) => { + if (!nav.hasAttribute('role')) { + nav.setAttribute('role', 'navigation'); + } + if (!nav.hasAttribute('aria-label')) { + nav.setAttribute('aria-label', `Navigation ${index + 1}`); + } + }); + const searchForm = document.querySelector('form[role="search"], .navbar-search'); + if (searchForm && !searchForm.hasAttribute('role')) { + searchForm.setAttribute('role', 'search'); + } + } + } + const initAccessibility = (config) => { + return new AccessibilityManager(config); + }; + + onDOMContentLoaded(() => { + const accessibilityManager = initAccessibility({ + announcements: true, + skipLinks: true, + focusManagement: true, + keyboardNavigation: true, + reducedMotion: true + }); + accessibilityManager.addLandmarks(); + }); + + exports.CardWidget = CardWidget; + exports.DirectChat = DirectChat; + exports.FullScreen = FullScreen; + exports.Layout = Layout; + exports.PushMenu = PushMenu; + exports.Treeview = Treeview; + exports.initAccessibility = initAccessibility; + +})); +//# sourceMappingURL=adminlte.js.map diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js.map b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js.map new file mode 100644 index 00000000..4344c78f --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.js.map @@ -0,0 +1 @@ +{"version":3,"file":"adminlte.js","sources":["../../src/ts/util/index.ts","../../src/ts/layout.ts","../../src/ts/card-widget.ts","../../src/ts/treeview.ts","../../src/ts/direct-chat.ts","../../src/ts/fullscreen.ts","../../src/ts/push-menu.ts","../../src/ts/accessibility.ts","../../src/ts/adminlte.ts"],"sourcesContent":[null,null,null,null,null,null,null,null,null],"names":["DATA_KEY","EVENT_KEY","EVENT_COLLAPSED","EVENT_EXPANDED","EVENT_MAXIMIZED","EVENT_MINIMIZED","Default","SELECTOR_DATA_TOGGLE"],"mappings":"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"} \ No newline at end of file diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js new file mode 100644 index 00000000..9182ae35 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js @@ -0,0 +1,7 @@ +/*! + * AdminLTE v4.0.0 (https://adminlte.io) + * Copyright 2014-2026 Colorlib + * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE) + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).adminlte={})}(this,function(e){"use strict";const t=[],n=e=>{"loading"===document.readyState?(t.length||document.addEventListener("DOMContentLoaded",()=>{for(const e of t)e()}),t.push(e)):e()},i=(e,t=500)=>{t<=1?e.style.display="none":(e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.boxSizing="border-box",e.style.height=`${e.offsetHeight}px`,e.style.overflow="hidden",globalThis.setTimeout(()=>{e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0"},1),globalThis.setTimeout(()=>{e.style.display="none",e.style.removeProperty("height"),e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t))},o=(e,t=500)=>{e.style.removeProperty("display");let{display:n}=globalThis.getComputedStyle(e);if("none"===n&&(n="block"),e.style.display=n,t<=1)return;const i=e.offsetHeight;e.style.overflow="hidden",e.style.height="0",e.style.paddingTop="0",e.style.paddingBottom="0",e.style.marginTop="0",e.style.marginBottom="0",globalThis.setTimeout(()=>{e.style.boxSizing="border-box",e.style.transitionProperty="height, margin, padding",e.style.transitionDuration=`${t}ms`,e.style.height=`${i}px`,e.style.removeProperty("padding-top"),e.style.removeProperty("padding-bottom"),e.style.removeProperty("margin-top"),e.style.removeProperty("margin-bottom")},1),globalThis.setTimeout(()=>{e.style.removeProperty("height"),e.style.removeProperty("overflow"),e.style.removeProperty("transition-duration"),e.style.removeProperty("transition-property")},t)},s="hold-transition";class a{_element;_holdTransitionTimer;constructor(e){this._element=e,this._holdTransitionTimer=void 0}holdTransition(e=100){this._holdTransitionTimer&&clearTimeout(this._holdTransitionTimer),document.body.classList.add(s),this._holdTransitionTimer=setTimeout(()=>{document.body.classList.remove(s)},e)}}n(()=>{const e=new a(document.body);window.addEventListener("resize",()=>e.holdTransition(200)),setTimeout(()=>{document.body.classList.add("app-loaded")},400)});const r=".lte.card-widget",l=`collapsed${r}`,c=`expanded${r}`,d=`remove${r}`,u=`maximized${r}`,m=`minimized${r}`,h="card",p="collapsed-card",g="collapsing-card",v="expanding-card",y="was-collapsed",b="maximized-card",f='[data-lte-toggle="card-remove"]',E='[data-lte-toggle="card-collapse"]',S='[data-lte-toggle="card-maximize"]',_=`.${h}`,L=".card-body",A=".card-footer",w={animationSpeed:500,collapseTrigger:E,removeTrigger:f,maximizeTrigger:S};class x{_element;_parent;_clone;_config;constructor(e,t){this._element=e,this._parent=e.closest(_),e.classList.contains(h)&&(this._parent=e),this._config={...w,...t}}collapse(){const e=new Event(l);if(this._parent){this._parent.classList.add(g);const e=this._parent?.querySelectorAll(`:scope > ${L}, :scope > ${A}`);e.forEach(e=>{e instanceof HTMLElement&&i(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&(this._parent.classList.add(p),this._parent.classList.remove(g))},this._config.animationSpeed)}this._element?.dispatchEvent(e)}expand(){const e=new Event(c);if(this._parent){this._parent.classList.add(v);const e=this._parent?.querySelectorAll(`:scope > ${L}, :scope > ${A}`);e.forEach(e=>{e instanceof HTMLElement&&o(e,this._config.animationSpeed)}),setTimeout(()=>{this._parent&&this._parent.classList.remove(p,v)},this._config.animationSpeed)}this._element?.dispatchEvent(e)}remove(){const e=new Event(d);this._parent&&i(this._parent,this._config.animationSpeed),this._element?.dispatchEvent(e)}toggle(){this._parent?.classList.contains(p)?this.expand():this.collapse()}maximize(){const e=new Event(u);this._parent&&(this._parent.style.height=`${this._parent.offsetHeight}px`,this._parent.style.width=`${this._parent.offsetWidth}px`,this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.add(b),this._parent&&(this._parent.classList.add(b),this._parent.classList.contains(p)&&this._parent.classList.add(y))},150)),this._element?.dispatchEvent(e)}minimize(){const e=new Event(m);this._parent&&(this._parent.style.height="auto",this._parent.style.width="auto",this._parent.style.transition="all .15s",setTimeout(()=>{const e=document.querySelector("html");e&&e.classList.remove(b),this._parent&&(this._parent.classList.remove(b),this._parent?.classList.contains(y)&&this._parent.classList.remove(y))},10)),this._element?.dispatchEvent(e)}toggleMaximize(){this._parent?.classList.contains(b)?this.minimize():this.maximize()}}n(()=>{document.querySelectorAll(E).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new x(t,w).toggle()})}),document.querySelectorAll(f).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new x(t,w).remove()})}),document.querySelectorAll(S).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target;new x(t,w).toggleMaximize()})})});const k=".lte.treeview",T=`expanded${k}`,q=`collapsed${k}`,M=`load${k}`,$="menu-open",P=".nav-item",N=".nav-treeview",R={animationSpeed:300,accordion:!0};class D{_element;_config;constructor(e,t){this._element=e,this._config={...R,...t}}open(){const e=new Event(T);if(this._config.accordion){const e=this._element.parentElement?.querySelectorAll(`${P}.${$}`);e?.forEach(e=>{if(e!==this._element.parentElement){e.classList.remove($);const t=e?.querySelector(N);t&&i(t,this._config.animationSpeed)}})}this._element.classList.add($);const t=this._element?.querySelector(N);t&&o(t,this._config.animationSpeed),this._element.dispatchEvent(e)}close(){const e=new Event(q);this._element.classList.remove($);const t=this._element?.querySelector(N);t&&i(t,this._config.animationSpeed),this._element.dispatchEvent(e)}toggle(){this._element.classList.contains($)?this.close():this.open()}}n(()=>{document.querySelectorAll(`${P}.${$}`).forEach(e=>{const t=e.querySelector(N);if(t){o(t,0);const n=new Event(M);e.dispatchEvent(n)}}),document.querySelectorAll('[data-lte-toggle="treeview"]').forEach(e=>{e.addEventListener("click",e=>{const t=e.target,n=t.closest(P),i=t.closest(".nav-link"),o=n?.querySelector(N),s=e.currentTarget;if(o&&("#"!==t?.getAttribute("href")&&"#"!==i?.getAttribute("href")||e.preventDefault(),n)){const e=s.dataset.accordion,t=s.dataset.animationSpeed,i={accordion:void 0===e?R.accordion:"true"===e,animationSpeed:void 0===t?R.animationSpeed:Number(t)};new D(n,i).toggle()}})})});const z=".lte.direct-chat",B=`expanded${z}`,F=`collapsed${z}`,C="direct-chat-contacts-open";class H{_element;constructor(e){this._element=e}toggle(){if(this._element.classList.contains(C)){const e=new Event(F);this._element.classList.remove(C),this._element.dispatchEvent(e)}else{const e=new Event(B);this._element.classList.add(C),this._element.dispatchEvent(e)}}}n(()=>{document.querySelectorAll('[data-lte-toggle="chat-pane"]').forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(".direct-chat");t&&new H(t).toggle()})})});const O=".lte.fullscreen",I=`maximized${O}`,K=`minimized${O}`,W='[data-lte-toggle="fullscreen"]',j='[data-lte-icon="maximize"]',U='[data-lte-icon="minimize"]';class V{_element;_config;constructor(e,t){this._element=e,this._config=t}inFullScreen(){const e=new Event(I),t=document.querySelector(j),n=document.querySelector(U);document.documentElement.requestFullscreen(),t&&t.classList.add("d-none"),n&&n.classList.remove("d-none"),this._element.dispatchEvent(e)}outFullscreen(){const e=new Event(K),t=document.querySelector(j),n=document.querySelector(U);document.exitFullscreen(),t&&t.classList.remove("d-none"),n&&n.classList.add("d-none"),this._element.dispatchEvent(e)}toggleFullScreen(){document.fullscreenEnabled&&(document.fullscreenElement?this.outFullscreen():this.inFullScreen())}}n(()=>{document.querySelectorAll(W).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();const t=e.target.closest(W);t&&new V(t,void 0).toggleFullScreen()})})});const G=".lte.push-menu",J=`open${G}`,Q=`collapse${G}`,X="sidebar-collapse",Y="sidebar-open",Z='[data-lte-toggle="sidebar"]',ee="lte.sidebar.state",te={sidebarBreakpoint:992,enablePersistence:!1};class ne{_element;_config;constructor(e,t){this._element=e,this._config={...te,...t}}isCollapsed(){return document.body.classList.contains(X)}isExplicitlyOpen(){return document.body.classList.contains(Y)}isMiniMode(){return document.body.classList.contains("sidebar-mini")}isMobileSize(){return globalThis.innerWidth<=this._config.sidebarBreakpoint}expand(){document.body.classList.remove(X),this.isMobileSize()&&document.body.classList.add(Y),this._element.dispatchEvent(new Event(J))}collapse(){document.body.classList.remove(Y),document.body.classList.add(X),this._element.dispatchEvent(new Event(Q))}toggle(){const e=this.isCollapsed();e?this.expand():this.collapse(),this._config.enablePersistence&&this.saveSidebarState(e?Y:X)}setupSidebarBreakPoint(){const e=document.querySelector('[class*="sidebar-expand"]');if(!e)return;const t=globalThis.getComputedStyle(e,"::before").getPropertyValue("content");if(!t||"none"===t)return;const n=Number(t.replace(/[^\d.-]/g,""));Number.isNaN(n)||(this._config={...this._config,sidebarBreakpoint:n})}updateStateByResponsiveLogic(){this.isMobileSize()?this.isExplicitlyOpen()||this.collapse():this.isMiniMode()&&this.isCollapsed()||this.expand()}saveSidebarState(e){if(void 0!==globalThis.localStorage)try{localStorage.setItem(ee,e)}catch{}}loadSidebarState(){if(void 0!==globalThis.localStorage)try{const e=localStorage.getItem(ee);e===X?this.collapse():e===Y?this.expand():this.updateStateByResponsiveLogic()}catch{this.updateStateByResponsiveLogic()}}clearSidebarState(){if(void 0!==globalThis.localStorage)try{localStorage.removeItem(ee)}catch{}}init(){this.setupSidebarBreakPoint(),this._config.enablePersistence||this.clearSidebarState(),this._config.enablePersistence&&!this.isMobileSize()?this.loadSidebarState():this.updateStateByResponsiveLogic()}}n(()=>{const e=document?.querySelector(".app-sidebar");if(!e)return;const t=e.dataset.sidebarBreakpoint,n=e.dataset.enablePersistence,i={sidebarBreakpoint:void 0===t?te.sidebarBreakpoint:Number(t),enablePersistence:void 0===n?te.enablePersistence:"true"===n},o=new ne(e,i);o.init(),window.addEventListener("resize",()=>{o.setupSidebarBreakPoint(),o.updateStateByResponsiveLogic()});const s=document.createElement("div");s.className="sidebar-overlay",document.querySelector(".app-wrapper")?.append(s);let a=!1;s.addEventListener("touchstart",()=>{a=!1},{passive:!0}),s.addEventListener("touchmove",()=>{a=!0},{passive:!0}),s.addEventListener("touchend",e=>{a||(e.preventDefault(),o.collapse()),a=!1},{passive:!1}),s.addEventListener("click",e=>{e.preventDefault(),o.collapse()}),document.querySelectorAll(Z).forEach(e=>{e.addEventListener("click",e=>{e.preventDefault();let t=e.currentTarget;"sidebar"!==t?.dataset.lteToggle&&(t=t?.closest(Z)),t&&(e?.preventDefault(),o.toggle())})})});class ie{config;liveRegion=null;focusHistory=[];constructor(e={}){this.config={announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0,...e},this.init()}init(){this.config.announcements&&this.createLiveRegion(),this.config.skipLinks&&this.addSkipLinks(),this.config.focusManagement&&this.initFocusManagement(),this.config.keyboardNavigation&&this.initKeyboardNavigation(),this.config.reducedMotion&&this.respectReducedMotion(),this.initErrorAnnouncements(),this.initTableAccessibility(),this.initFormAccessibility()}createLiveRegion(){this.liveRegion||(this.liveRegion=document.createElement("div"),this.liveRegion.id="live-region",this.liveRegion.className="live-region",this.liveRegion.setAttribute("aria-live","polite"),this.liveRegion.setAttribute("aria-atomic","true"),this.liveRegion.setAttribute("role","status"),document.body.append(this.liveRegion))}addSkipLinks(){const e=document.createElement("div");e.className="skip-links";const t=document.createElement("a");t.href="#main",t.className="skip-link",t.textContent="Skip to main content";const n=document.createElement("a");n.href="#navigation",n.className="skip-link",n.textContent="Skip to navigation",e.append(t),e.append(n),document.body.insertBefore(e,document.body.firstChild),this.ensureSkipTargets()}ensureSkipTargets(){const e=document.querySelector('#main, main, [role="main"]');e&&!e.id&&(e.id="main"),e&&!e.hasAttribute("tabindex")&&e.setAttribute("tabindex","-1");const t=document.querySelector('#navigation, nav, [role="navigation"]');t&&!t.id&&(t.id="navigation"),t&&!t.hasAttribute("tabindex")&&t.setAttribute("tabindex","-1")}initFocusManagement(){document.addEventListener("keydown",e=>{"Tab"===e.key&&this.handleTabNavigation(e),"Escape"===e.key&&this.handleEscapeKey(e)}),this.initModalFocusManagement(),this.initDropdownFocusManagement()}handleTabNavigation(e){const t=this.getFocusableElements(),n=t.indexOf(document.activeElement);e.shiftKey?n<=0&&(e.preventDefault(),t.at(-1)?.focus()):n>=t.length-1&&(e.preventDefault(),t[0]?.focus())}getFocusableElements(){const e=["a[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])','[contenteditable="true"]'].join(", ");return Array.from(document.querySelectorAll(e))}handleEscapeKey(e){if(!document.querySelector(".modal.show")&&document.querySelector(".dropdown-menu.show")){const t=document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]');t?.click(),e.preventDefault()}}initKeyboardNavigation(){document.addEventListener("keydown",e=>{const t=e.target;t.closest(".nav, .navbar-nav, .dropdown-menu")&&this.handleMenuNavigation(e),"Enter"!==e.key&&" "!==e.key||!t.hasAttribute("role")||"button"!==t.getAttribute("role")||t.matches('button, input[type="button"], input[type="submit"]')||(e.preventDefault(),t.click())})}handleMenuNavigation(e){if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End"].includes(e.key))return;const t=e.target,n=Array.from(t.closest(".nav, .navbar-nav, .dropdown-menu")?.querySelectorAll("a, button")||[]),i=n.indexOf(t);let o;switch(e.key){case"ArrowDown":case"ArrowRight":o=i0?i-1:n.length-1;break;case"Home":o=0;break;case"End":o=n.length-1;break;default:return}e.preventDefault(),n[o]?.focus()}respectReducedMotion(){if(globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches){document.body.classList.add("reduce-motion"),document.documentElement.style.scrollBehavior="auto";const e=document.createElement("style");e.textContent="\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n ",document.head.append(e)}}initErrorAnnouncements(){new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{if(e.nodeType===Node.ELEMENT_NODE){const t=e;t.matches(".alert-danger, .invalid-feedback, .error")&&this.announce(t.textContent||"Error occurred","assertive"),t.matches(".alert-success, .success")&&this.announce(t.textContent||"Success","polite")}})})}).observe(document.body,{childList:!0,subtree:!0})}initTableAccessibility(){document.querySelectorAll("table").forEach(e=>{if(e.hasAttribute("role")||e.setAttribute("role","table"),e.querySelectorAll("th").forEach(e=>{if(!e.hasAttribute("scope")){const t=e.closest("thead"),n=0===e.cellIndex;t?e.setAttribute("scope","col"):n&&e.setAttribute("scope","row")}}),!e.querySelector("caption")&&e.hasAttribute("title")){const t=document.createElement("caption");t.textContent=e.getAttribute("title")||"",e.insertBefore(t,e.firstChild)}})}initFormAccessibility(){document.querySelectorAll("input, select, textarea").forEach(e=>{const t=e;if(!t.labels?.length&&!t.hasAttribute("aria-label")&&!t.hasAttribute("aria-labelledby")){const e=t.getAttribute("placeholder");e&&t.setAttribute("aria-label",e)}if(t.hasAttribute("required")){const e=t.labels?.[0];if(e&&!e.querySelector(".required-indicator")){const t=document.createElement("span");t.className="required-indicator sr-only",t.textContent=" (required)",e.append(t)}}t.classList.contains("disable-adminlte-validations")||t.addEventListener("invalid",()=>{this.handleFormError(t)})})}handleFormError(e){const t=`${e.id||e.name}-error`;let n=document.getElementById(t);n||(n=document.createElement("div"),n.id=t,n.className="invalid-feedback",n.setAttribute("role","alert"),e.parentNode?.append(n)),n.textContent=e.validationMessage,e.setAttribute("aria-describedby",t),e.classList.add("is-invalid"),this.announce(`Error in ${e.labels?.[0]?.textContent||e.name}: ${e.validationMessage}`,"assertive")}initModalFocusManagement(){document.addEventListener("shown.bs.modal",e=>{const t=e.target.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');t.length>0&&t[0].focus(),this.focusHistory.push(document.activeElement)}),document.addEventListener("hidden.bs.modal",()=>{const e=this.focusHistory.pop();e&&e.focus()})}initDropdownFocusManagement(){document.addEventListener("shown.bs.dropdown",e=>{const t=e.target.querySelector(".dropdown-menu"),n=t?.querySelector("a, button");n&&n.focus()})}announce(e,t="polite"){this.liveRegion||this.createLiveRegion(),this.liveRegion&&(this.liveRegion.setAttribute("aria-live",t),this.liveRegion.textContent=e,setTimeout(()=>{this.liveRegion&&(this.liveRegion.textContent="")},1e3))}focusElement(e){const t=document.querySelector(e);t&&(t.focus(),t.scrollIntoView({behavior:"smooth",block:"center"}))}trapFocus(e){const t=e.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),n=Array.from(t),i=n[0],o=n.at(-1);e.addEventListener("keydown",e=>{"Tab"===e.key&&(e.shiftKey?document.activeElement===i&&(o?.focus(),e.preventDefault()):document.activeElement===o&&(i.focus(),e.preventDefault()))})}addLandmarks(){if(!document.querySelector("main")){const e=document.querySelector(".app-main");e&&(e.setAttribute("role","main"),e.id="main")}document.querySelectorAll(".navbar-nav, .nav").forEach((e,t)=>{e.hasAttribute("role")||e.setAttribute("role","navigation"),e.hasAttribute("aria-label")||e.setAttribute("aria-label",`Navigation ${t+1}`)});const e=document.querySelector('form[role="search"], .navbar-search');e&&!e.hasAttribute("role")&&e.setAttribute("role","search")}}const oe=e=>new ie(e);n(()=>{oe({announcements:!0,skipLinks:!0,focusManagement:!0,keyboardNavigation:!0,reducedMotion:!0}).addLandmarks()}),e.CardWidget=x,e.DirectChat=H,e.FullScreen=V,e.Layout=a,e.PushMenu=ne,e.Treeview=D,e.initAccessibility=oe}); +//# sourceMappingURL=adminlte.min.js.map \ No newline at end of file diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js.map b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js.map new file mode 100644 index 00000000..8d1f43a3 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/js/adminlte.min.js.map @@ -0,0 +1 @@ +{"version":3,"names":["domContentLoadedCallbacks","onDOMContentLoaded","callback","document","readyState","length","addEventListener","push","slideUp","target","duration","style","display","transitionProperty","transitionDuration","boxSizing","height","offsetHeight","overflow","globalThis","setTimeout","paddingTop","paddingBottom","marginTop","marginBottom","removeProperty","slideDown","getComputedStyle","CLASS_NAME_HOLD_TRANSITIONS","Layout","_element","_holdTransitionTimer","constructor","element","this","undefined","holdTransition","time","clearTimeout","body","classList","add","remove","layout","window","EVENT_KEY","EVENT_COLLAPSED","EVENT_EXPANDED","EVENT_REMOVE","EVENT_MAXIMIZED","EVENT_MINIMIZED","CLASS_NAME_CARD","CLASS_NAME_COLLAPSED","CLASS_NAME_COLLAPSING","CLASS_NAME_EXPANDING","CLASS_NAME_WAS_COLLAPSED","CLASS_NAME_MAXIMIZED","SELECTOR_DATA_REMOVE","SELECTOR_DATA_COLLAPSE","SELECTOR_DATA_MAXIMIZE","SELECTOR_CARD","SELECTOR_CARD_BODY","SELECTOR_CARD_FOOTER","Default","animationSpeed","collapseTrigger","removeTrigger","maximizeTrigger","CardWidget","_parent","_clone","_config","config","closest","contains","collapse","event","Event","elm","querySelectorAll","forEach","el","HTMLElement","dispatchEvent","expand","toggle","maximize","width","offsetWidth","transition","htmlTag","querySelector","minimize","toggleMaximize","btn","preventDefault","EVENT_LOAD_DATA_API","CLASS_NAME_MENU_OPEN","SELECTOR_NAV_ITEM","SELECTOR_TREEVIEW_MENU","accordion","Treeview","open","openMenuList","parentElement","openMenu","childElement","close","menuItem","targetItem","targetLink","targetTreeviewMenu","lteToggleElement","currentTarget","getAttribute","accordionAttr","dataset","animationSpeedAttr","Number","CLASS_NAME_DIRECT_CHAT_OPEN","DirectChat","chatPane","SELECTOR_FULLSCREEN_TOGGLE","SELECTOR_MAXIMIZE_ICON","SELECTOR_MINIMIZE_ICON","FullScreen","inFullScreen","iconMaximize","iconMinimize","documentElement","requestFullscreen","outFullscreen","exitFullscreen","toggleFullScreen","fullscreenEnabled","fullscreenElement","button","EVENT_OPEN","EVENT_COLLAPSE","CLASS_NAME_SIDEBAR_COLLAPSE","CLASS_NAME_SIDEBAR_OPEN","SELECTOR_SIDEBAR_TOGGLE","STORAGE_KEY_SIDEBAR_STATE","Defaults","sidebarBreakpoint","enablePersistence","PushMenu","isCollapsed","isExplicitlyOpen","isMiniMode","isMobileSize","innerWidth","saveSidebarState","setupSidebarBreakPoint","sidebarExpand","content","getPropertyValue","breakpointValue","replace","isNaN","updateStateByResponsiveLogic","state","localStorage","setItem","loadSidebarState","storedState","getItem","clearSidebarState","removeItem","init","sidebar","sidebarBreakpointAttr","enablePersistenceAttr","pushMenu","sidebarOverlay","createElement","className","append","overlayTouchMoved","passive","lteToggle","AccessibilityManager","liveRegion","focusHistory","announcements","skipLinks","focusManagement","keyboardNavigation","reducedMotion","createLiveRegion","addSkipLinks","initFocusManagement","initKeyboardNavigation","respectReducedMotion","initErrorAnnouncements","initTableAccessibility","initFormAccessibility","id","setAttribute","skipLinksContainer","skipToMain","href","textContent","skipToNav","insertBefore","firstChild","ensureSkipTargets","main","hasAttribute","nav","key","handleTabNavigation","handleEscapeKey","initModalFocusManagement","initDropdownFocusManagement","focusableElements","getFocusableElements","currentIndex","indexOf","activeElement","shiftKey","at","focus","selector","join","Array","from","toggleButton","click","handleMenuNavigation","matches","includes","currentElement","menuItems","nextIndex","matchMedia","scrollBehavior","head","MutationObserver","mutations","mutation","addedNodes","node","nodeType","Node","ELEMENT_NODE","announce","observe","childList","subtree","table","th","isInThead","isFirstColumn","cellIndex","caption","input","htmlInput","labels","placeholder","label","indicator","handleFormError","errorId","name","errorElement","getElementById","parentNode","validationMessage","previousElement","pop","menu","firstItem","message","priority","focusElement","scrollIntoView","behavior","block","trapFocus","container","focusableArray","firstElement","lastElement","addLandmarks","appMain","index","searchForm","initAccessibility"],"sources":["../../src/ts/util/index.ts","../../src/ts/layout.ts","../../src/ts/card-widget.ts","../../src/ts/treeview.ts","../../src/ts/direct-chat.ts","../../src/ts/fullscreen.ts","../../src/ts/push-menu.ts","../../src/ts/accessibility.ts","../../src/ts/adminlte.ts"],"mappings":"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","ignoreList":[]} \ No newline at end of file diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_accessibility.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_accessibility.scss new file mode 100644 index 00000000..a07fc9bd --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_accessibility.scss @@ -0,0 +1,337 @@ +/* ========================================================================== + AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance + ========================================================================== */ + +/* Skip Links - WCAG 2.4.1: Bypass Blocks */ +.skip-link { + position: absolute; + top: -40px; + left: 6px; + z-index: 999999; + padding: 8px 16px; + font-weight: 600; + color: var(--bs-white); + text-decoration: none; + background: var(--bs-primary); + + &:focus { + top: 0; + outline: 3px solid var(--bs-warning); + outline-offset: 2px; + } + + &:hover { + color: var(--bs-white); + text-decoration: none; + background: var(--bs-primary-emphasis); + } +} + +/* Enhanced Focus Indicators - WCAG 2.4.7: Focus Visible */ +.focus-enhanced { + &:focus { + outline: 3px solid var(--bs-focus-ring-color, #0d6efd); + outline-offset: 2px; + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); + } +} + +/* High Contrast Mode Support */ +@media (prefers-contrast: high) { + .card { + border: 2px solid; + } + + .btn { + border-width: 2px; + } + + .nav-link { + border: 1px solid transparent; + + &:hover, + &:focus { + border-color: currentcolor; + } + } +} + +/* Reduced Motion Support - WCAG 2.3.3: Animation from Interactions */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition-duration: .01ms !important; + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + } + + .fade { + opacity: 1 !important; + /* stylelint-disable-next-line property-disallowed-list */ + transition: none !important; + } + + .collapse { + /* stylelint-disable-next-line property-disallowed-list */ + transition: none !important; + } + + .modal.fade .modal-dialog { + transform: none !important; + } +} + +/* Screen Reader Only Content */ +.sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +.sr-only-focusable:focus { + position: static !important; + width: auto !important; + height: auto !important; + padding: inherit !important; + margin: inherit !important; + overflow: visible !important; + clip: auto !important; + white-space: normal !important; +} + +/* Focus Trap Utilities */ +.focus-trap { + &:focus { + box-shadow: 0 0 0 .25rem rgba(13, 110, 253, .25); + } +} + +/* Accessible Color Combinations - WCAG 1.4.3: Contrast (Minimum) */ +.text-accessible-primary { + color: #003d82; /* 4.5:1 contrast on white */ +} + +.text-accessible-success { + color: #0f5132; /* 4.5:1 contrast on white */ +} + +.text-accessible-danger { + color: #842029; /* 4.5:1 contrast on white */ +} + +.text-accessible-warning { + color: #664d03; /* 4.5:1 contrast on white */ +} + +/* ARIA Live Regions */ +.live-region { + position: absolute; + left: -10000px; + width: 1px; + height: 1px; + overflow: hidden; + + &.live-region-visible { + position: static; + left: auto; + width: auto; + height: auto; + overflow: visible; + } +} + +/* Enhanced Error States - WCAG 3.3.1: Error Identification */ +.form-control.is-invalid { + border-color: var(--bs-danger); + + &:focus { + border-color: var(--bs-danger); + box-shadow: 0 0 0 .25rem rgba(220, 53, 69, .25); + } +} + +.invalid-feedback { + &[role="alert"] { + font-weight: 600; + } +} + +/* Target Size - WCAG 2.5.8: Target Size (Minimum) */ +.touch-target { + min-width: 44px; + min-height: 44px; + + &.touch-target-small { + min-width: 24px; + min-height: 24px; + } +} + +/* Table Accessibility */ +.table-accessible { + th { + font-weight: 600; + background-color: var(--bs-secondary-bg); + + &[scope="col"] { + border-bottom: 2px solid var(--bs-border-color); + } + + &[scope="row"] { + border-right: 2px solid var(--bs-border-color); + } + } + + caption { + padding: .75rem; + font-weight: 600; + color: var(--bs-secondary); + text-align: left; + caption-side: top; + } +} + +/* Navigation Landmarks */ +nav[role="navigation"] { + &:not([aria-label]):not([aria-labelledby]) { + &::before { + position: absolute; + left: -10000px; + content: "Navigation"; + } + } +} + +/* Form Fieldset Styling */ +fieldset { + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid var(--bs-border-color); + + legend { + padding: 0 .5rem; + margin-bottom: .5rem; + font-size: 1.1em; + font-weight: 600; + } +} + +/* Loading States */ +.loading[aria-busy="true"] { + position: relative; + pointer-events: none; + + &::after { + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + content: ""; + border: 2px solid var(--bs-primary); + border-top-color: transparent; + animation: spin 1s linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + &::after { + border-top-color: var(--bs-primary); + animation: none; + } + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Dark Mode Accessibility */ +[data-bs-theme="dark"] { + .text-accessible-primary { + color: #6ea8fe; + } + + .text-accessible-success { + color: #75b798; + } + + .text-accessible-danger { + color: #f1aeb5; + } + + .text-accessible-warning { + color: #ffda6a; + } +} + +/* Print Accessibility */ +@media print { + .skip-link, + .btn, + .nav-link { + color: #000 !important; + background: transparent !important; + border: 1px solid #000 !important; + } + + a[href^="http"]::after { + font-size: .8em; + content: " (" attr(href) ")"; + } + + /* Print Layout Fix - Ensure sidebar and main content are both visible */ + .app-wrapper { + display: grid !important; + grid-template-rows: auto 1fr auto !important; + grid-template-columns: auto 1fr !important; + } + + .sidebar-overlay { + display: none !important; + } + + .app-sidebar { + position: static !important; + display: block !important; + min-width: 200px !important; + max-width: 200px !important; + max-height: none !important; + margin-left: 0 !important; + overflow: visible !important; + } + + .sidebar-wrapper { + height: auto !important; + overflow: visible !important; + } + + .app-header { + position: static !important; + } + + .app-main { + width: auto !important; + max-width: 100% !important; + overflow: visible !important; + } + + .app-content { + overflow: visible !important; + } + + .app-footer { + position: static !important; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-content.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-content.scss new file mode 100644 index 00000000..d9dfcb9b --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-content.scss @@ -0,0 +1,3 @@ +.app-content { + padding: $lte-content-padding-y $lte-content-padding-x; +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-footer.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-footer.scss new file mode 100644 index 00000000..afa9e037 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-footer.scss @@ -0,0 +1,32 @@ +// +// Core: Main Footer +// + +.app-footer { + grid-area: #{$lte-prefix}app-footer; + width: inherit; + max-width: 100vw; + min-height: 3rem; + padding: $lte-app-footer-padding; + color: $lte-app-footer-color; + background-color: $lte-app-footer-bg; + border-top: $lte-app-footer-border-top; + @include transition($lte-transition-speed $lte-transition-fn); +} + +.fixed-footer { + .app-footer { + position: sticky; + bottom: 0; + z-index: $lte-zindex-fixed-footer; + } + + // When layout-fixed is used, app-main has overflow: auto which prevents + // position: sticky from working on the footer. Ensure the grid keeps the + // footer pinned at the bottom while app-main scrolls independently. + &.layout-fixed { + .app-main { + min-height: 0; + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-header.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-header.scss new file mode 100644 index 00000000..0d9aff9b --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-header.scss @@ -0,0 +1,61 @@ +// +// Core: Main Header +// + +.app-header { + z-index: $lte-zindex-app-header; + grid-area: #{$lte-prefix}app-header; + max-width: 100vw; + border-bottom: $lte-app-header-bottom-border; + @include transition($lte-transition-speed $lte-transition-fn); + + .nav-link { + position: relative; + height: $nav-link-height; + } +} + +// Navbar badge +.navbar-badge { + position: absolute; + top: 9px; + right: 5px; + padding: 2px 4px; + font-size: .6rem; + font-weight: 400; +} + +.fixed-header { + .app-header { + position: sticky; + top: 0; + z-index: $lte-zindex-fixed-header; + } + + // Fixes #6020: when only `fixed-header` is used (without `layout-fixed`), + // the sidebar — including its branding — used to scroll with the page + // because nothing made it sticky. Pin the sidebar so the brand and menu + // stay visible while the page content scrolls beneath the fixed header. + // Only applied on `sidebar-expand-*` breakpoints, since the mobile sidebar + // is an off-canvas overlay that handles its own positioning. + @each $breakpoint in map-keys($grid-breakpoints) { + $next: breakpoint-next($breakpoint, $grid-breakpoints); + $infix: breakpoint-infix($next, $grid-breakpoints); + + &.sidebar-expand#{$infix} { + @include media-breakpoint-up($next) { + .app-sidebar { + position: sticky; + top: 0; + max-height: 100vh; + + .sidebar-wrapper { + height: subtract(100vh, add($lte-app-header-height, 1px)); + overflow-x: hidden; + overflow-y: auto; + } + } + } + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-main.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-main.scss new file mode 100644 index 00000000..617cf045 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-main.scss @@ -0,0 +1,41 @@ +.app-main { + position: relative; + display: flex; + flex-direction: column; + grid-area: #{$lte-prefix}app-main; + max-width: 100vw; + padding-bottom: $lte-app-main-padding-bottom; + @include transition($lte-transition-speed $lte-transition-fn); + + .app-content-header { + padding: 1rem $lte-content-padding-x; + + .breadcrumb { + padding: 0; + margin-bottom: 0; + line-height: 2.5rem; + + a { + text-decoration: none; + } + } + } + + .app-content-top-area, + .app-content-bottom-area { + color: $lte-app-content-bottom-area-color; + background-color: $lte-app-content-bottom-area-bg; + } + + .app-content-top-area { + padding: $lte-app-content-top-area-padding-y $lte-app-content-top-area-padding-x; + border-bottom: $lte-app-content-top-area-top-border; + } + + .app-content-bottom-area { + padding: $lte-app-content-bottom-area-padding-y $lte-app-content-bottom-area-padding-x; + margin-top: auto; + margin-bottom: $lte-app-content-bottom-area-margin-bottom; + border-top: $lte-app-content-bottom-area-top-border; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-sidebar.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-sidebar.scss new file mode 100644 index 00000000..5bf4ff05 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-sidebar.scss @@ -0,0 +1,619 @@ +.app-sidebar { + --#{$lte-prefix}sidebar-hover-bg: #{$lte-sidebar-hover-bg}; + --#{$lte-prefix}sidebar-color: #{$lte-sidebar-color}; + --#{$lte-prefix}sidebar-hover-color: #{$lte-sidebar-hover-color}; + --#{$lte-prefix}sidebar-active-color: #{$lte-sidebar-active-color}; + --#{$lte-prefix}sidebar-menu-active-bg: #{$lte-sidebar-menu-active-bg}; + --#{$lte-prefix}sidebar-menu-active-color: #{$lte-sidebar-menu-active-color}; + --#{$lte-prefix}sidebar-submenu-bg: #{$lte-sidebar-submenu-bg}; + --#{$lte-prefix}sidebar-submenu-color: #{$lte-sidebar-submenu-color}; + --#{$lte-prefix}sidebar-submenu-hover-color: #{$lte-sidebar-submenu-hover-color}; + --#{$lte-prefix}sidebar-submenu-hover-bg: #{$lte-sidebar-submenu-hover-bg}; + --#{$lte-prefix}sidebar-submenu-active-color: #{$lte-sidebar-submenu-active-color}; + --#{$lte-prefix}sidebar-submenu-active-bg: #{$lte-sidebar-submenu-active-bg}; + --#{$lte-prefix}sidebar-header-color: #{$lte-sidebar-header-color}; + + z-index: $lte-zindex-sidebar; + grid-area: #{$lte-prefix}app-sidebar; + min-width: var(--#{$lte-prefix}sidebar-width); + max-width: var(--#{$lte-prefix}sidebar-width); + @include transition($lte-sidebar-transition); +} + +.sidebar-brand { + display: flex; + align-items: center; + justify-content: center; + height: $lte-app-header-height; + padding: $lte-brand-link-padding-y $lte-brand-link-padding-x; + overflow: hidden; + font-size: $navbar-brand-font-size; + white-space: nowrap; + border-bottom: $lte-brand-link-border-buttom solid var(--#{$prefix}border-color); + @include transition(width $lte-transition-speed $lte-transition-fn); + + .brand-link { + display: flex; + align-items: center; + text-decoration: none; + + .brand-image { + float: left; + width: auto; + max-height: 33px; + line-height: .8; + } + + .brand-image-xs { + float: left; + width: auto; + max-height: 33px; + margin-top: -.1rem; + line-height: .8; + } + + .brand-image-xl { + width: auto; + max-height: 40px; + line-height: .8; + + &.single { + margin-top: -.3rem; + } + } + } + + .brand-text { + margin-left: .5rem; + color: rgba(var(--#{$prefix}emphasis-color-rgb), .8); + @include transition(flex $lte-transition-speed $lte-transition-fn, width $lte-transition-speed $lte-transition-fn); + &:hover { + color: var(--#{$prefix}emphasis-color); + } + } +} + +.sidebar-wrapper { + padding-top: $lte-sidebar-padding-y; + padding-right: $lte-sidebar-padding-x; + padding-bottom: $lte-sidebar-padding-y; + padding-left: $lte-sidebar-padding-x; + overscroll-behavior: contain; + @include scrollbar-color-gray(); + @include scrollbar-width-thin(); + + .nav-item { + max-width: 100%; + } + + .nav-link { + display: flex; + justify-content: flex-start; + + p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .nav-icon { + display: flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + max-width: 1.5rem; + } + + // Sidebar Menu. First level links + .sidebar-menu > .nav-item { + // links + > .nav-link { + color: var(--#{$lte-prefix}sidebar-color); + + &:hover, + &:focus { + color: var(--#{$lte-prefix}sidebar-hover-color); + background-color: var(--#{$lte-prefix}sidebar-hover-bg); + } + + &.active { + color: var(--#{$lte-prefix}sidebar-menu-active-color); + background-color: var(--#{$lte-prefix}sidebar-menu-active-bg); + } + } + + // Open state + &.menu-open > .nav-link { + color: var(--#{$lte-prefix}sidebar-hover-color); + background-color: var(--#{$lte-prefix}sidebar-hover-bg); + + &.active { + color: var(--#{$lte-prefix}sidebar-menu-active-color); + background-color: var(--#{$lte-prefix}sidebar-menu-active-bg); + } + } + + // First Level Submenu + > .nav-treeview { + background-color: var(--#{$lte-prefix}sidebar-submenu-bg); + } + } + + // Section Heading + .nav-header { + display: inline; + overflow: hidden; + color: var(--#{$lte-prefix}sidebar-header-color); + text-overflow: ellipsis; + background-color: inherit; + } + + // All links within the sidebar menu + a { + color: var(--#{$lte-prefix}sidebar-color); + } + + // All submenus + .nav-treeview { + > .nav-item { + > .nav-link { + color: var(--#{$lte-prefix}sidebar-submenu-color); + + &:hover, + &:focus { + color: var(--#{$lte-prefix}sidebar-submenu-hover-color); + background-color: var(--#{$lte-prefix}sidebar-submenu-hover-bg); + } + + &.active { + color: var(--#{$lte-prefix}sidebar-submenu-active-color); + background-color: var(--#{$lte-prefix}sidebar-submenu-active-bg); + } + } + } + } +} + +// Sidebar navigation menu +.sidebar-menu { + // All levels + .nav-item { + > .nav-link { + margin-bottom: .2rem; + + .nav-arrow { + @include transition(transform $lte-transition-fn $lte-transition-speed); + transform: translateY(-50%) #{"/*rtl:append:rotate(180deg)*/"}; + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + } + } + + // All levels + .nav-link > .nav-badge, + .nav-link > p > .nav-badge { + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + } + + .nav-link > .nav-arrow, + .nav-link > p > .nav-arrow { + position: absolute; + top: 50%; + right: 1rem; + } + + .nav-link { + position: relative; + width: 100%; + @include transition(width $lte-transition-fn $lte-transition-speed); + @include border-radius($border-radius); + + p { + display: inline; + padding-left: .5rem; + margin: 0; + } + } + + .nav-header { + position: relative; + width: 100%; + padding: $nav-link-padding-y ($nav-link-padding-y * 1.5); + font-size: .9rem; + @include transition(width $lte-transition-fn $lte-transition-speed); + } + + // Tree view menu + .nav-treeview { + display: none; + padding: 0; + list-style: none; + } + + .menu-open { + > .nav-treeview { + display: block; + } + + > .nav-link { + .nav-arrow { + transform: translateY(-50%) rotate(90deg) #{"/*rtl:ignore*/"}; + } + } + } + + // Override: ensure badges/arrows are aligned to the end (right in LTR, left in RTL) + .nav-link > .nav-badge, + .nav-link > p > .nav-badge, + .nav-link > .nav-arrow, + .nav-link > p > .nav-arrow { + right: 1rem !important; // place at end in LTR + left: auto !important; + } +} + +.nav-indent { + .nav-treeview { + padding-left: $lte-sidebar-padding-x; + } +} + +.nav-compact.nav-indent { + .nav-treeview { + padding-left: 0; + + .nav-item { + padding-left: $lte-sidebar-padding-x; + } + } +} + +.sidebar-mini.sidebar-collapse.nav-indent { + .app-sidebar:hover { + .nav-treeview { + padding-left: 0; + + .nav-item { + padding-left: $lte-sidebar-padding-x; + } + } + } +} + +.sidebar-collapse { + &.nav-compact.nav-indent { + .nav-treeview { + .nav-item { + padding-left: 0; + } + } + } +} + +.nav-compact .nav-link { + @include border-radius(0); + margin-bottom: 0 !important; +} + +// A fix for text overflow while transitioning from sidebar mini to full sidebar +.sidebar-menu, +.sidebar-menu > .nav-header, +.sidebar-menu .nav-link { + white-space: nowrap; +} + +// Logo style +.logo-xs, +.logo-xl { + position: absolute; + visibility: visible; + opacity: 1; + + &.brand-image-xs { + top: 12px; + left: 18px; + } + + &.brand-image-xl { + top: 6px; + left: 12px; + } +} + +.logo-xs { + visibility: hidden; + opacity: 0; + + &.brand-image-xl { + top: 8px; + left: 16px; + } +} + +.brand-link { + &.logo-switch { + &::before { + content: "\00a0"; + } + } +} + +.sidebar-mini.sidebar-collapse { + .app-sidebar { + min-width: $lte-sidebar-mini-width; + max-width: $lte-sidebar-mini-width; + } + + // Make the sidebar headers + .sidebar-menu .nav-header { + display: none; + } + + .sidebar-menu { + .nav-link { + width: $lte-sidebar-mini-width - $lte-sidebar-padding-x * 2; + + p { + display: inline-block; + width: 0; + white-space: nowrap; + } + } + .nav-badge, + .nav-arrow { + display: none; + animation-name: fadeOut; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + } + + .brand-text { + display: inline-block; + max-width: 0; + overflow: hidden; + } + + .sidebar-menu .nav-link p, + .brand-text, + .logo-xl, + .nav-arrow { + visibility: hidden; + animation-name: fadeOut; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + + + .logo-xs { + display: inline-block; + visibility: visible; + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + + &:not(.sidebar-without-hover) { + .app-sidebar:hover { + min-width: var(--#{$lte-prefix}sidebar-width); + max-width: var(--#{$lte-prefix}sidebar-width); + + .sidebar-menu .nav-header { + display: inline; + } + + .sidebar-menu .nav-link { + width: auto; + } + + .sidebar-menu .nav-link p, + .brand-text, + .logo-xl { + width: auto; + margin-left: 0; + visibility: visible; + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + + .brand-text { + display: inline; + max-width: inherit; + margin-left: .5rem; + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + + .nav-badge, + .nav-arrow { + display: inline-block; + visibility: visible; + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + animation-delay: $lte-transition-speed; + } + + .nav-link p { + padding-left: .5rem; + } + + .logo-xs { + visibility: hidden; + animation-name: fadeOut; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + } + } + } +} + +.sidebar-collapse:not(.sidebar-mini) { + .app-sidebar { + margin-left: calc(var(--#{$lte-prefix}sidebar-width) * -1); // stylelint-disable-line function-disallowed-list + } +} + +.sidebar-expand { + @each $breakpoint in map-keys($grid-breakpoints) { + $next: breakpoint-next($breakpoint, $grid-breakpoints); + $infix: breakpoint-infix($next, $grid-breakpoints); + + /* stylelint-disable-next-line scss/selector-no-union-class-name */ + &#{$infix} { + @include media-breakpoint-up($next) { + &.layout-fixed { + .app-main-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; + } + .app-sidebar-wrapper { + position: relative; + } + .app-main { + flex: 1 1 auto; + overflow: auto; + } + .app-sidebar { + position: sticky; + top: 0; + bottom: 0; + max-height: 100vh; + + .sidebar-wrapper { + height: subtract(100vh, add($lte-app-header-height, 1px)); + overflow-x: hidden; + overflow-y: auto; + } + } + } + + &.sidebar-open { + .nav-link > .nav-badge, + .nav-link > p > .nav-badge { + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + animation-delay: $lte-transition-speed; + } + .nav-link > .nav-arrow, + .nav-link > p > .nav-arrow { + animation-name: fadeIn; + animation-duration: $lte-transition-speed; + animation-fill-mode: both; + animation-delay: $lte-transition-speed; + } + } + } + + @include media-breakpoint-down($next) { + $max: breakpoint-max($next); + + &::before { + display: none; + content: "#{$max}"; + } + + .app-sidebar { + position: fixed; + top: 0; + bottom: 0; + max-height: 100vh; + margin-left: calc(var(--#{$lte-prefix}sidebar-width) * -1); // stylelint-disable-line function-disallowed-list + + .sidebar-wrapper { + height: subtract(100vh, add($lte-app-header-height, 1px)); + overflow-x: hidden; + overflow-y: auto; + } + } + + &.sidebar-open { + .app-sidebar { + margin-left: 0; + } + + .sidebar-overlay { + position: absolute; + inset: 0; + z-index: $lte-zindex-sidebar-overlay; + width: 100%; + height: 100%; + cursor: pointer; + visibility: visible; + background-color: rgba(0, 0, 0, .2); + animation-name: fadeIn; + animation-fill-mode: both; + } + } + } + } + } +} + +.sidebar-menu .nav-link p, +.app-sidebar .brand-text, +.app-sidebar .logo-xs, +.app-sidebar .logo-xl { + @include transition(margin-left $lte-transition-speed linear, opacity $lte-transition-speed ease, visibility $lte-transition-speed ease); +} + +// To prevent onload transition and animation +.app-loaded { + &.sidebar-mini.sidebar-collapse { + .sidebar-menu .nav-link p, + .brand-text { + animation-duration: $lte-transition-speed; + } + } +} +body:not(.app-loaded) { + .app-header, + .app-sidebar, + .app-main, + .app-footer { + @include transition(none !important); + animation-duration: 0s !important; + } +} +.hold-transition { + .app-header, + .app-sidebar, + .app-main, + .app-footer, + .nav-arrow, + .nav-badge { + @include transition(none !important); + animation-duration: 0s !important; + } +} + +@if $enable-dark-mode { + @include color-mode(dark) { + &.app-sidebar, + .app-sidebar { + --#{$lte-prefix}sidebar-hover-bg: #{$lte-sidebar-hover-bg-dark}; + --#{$lte-prefix}sidebar-color: #{$lte-sidebar-color-dark}; + --#{$lte-prefix}sidebar-hover-color: #{$lte-sidebar-hover-color-dark}; + --#{$lte-prefix}sidebar-active-color: #{$lte-sidebar-active-color-dark}; + --#{$lte-prefix}sidebar-menu-active-bg: #{$lte-sidebar-menu-active-bg-dark}; + --#{$lte-prefix}sidebar-menu-active-color: #{$lte-sidebar-menu-active-color-dark}; + --#{$lte-prefix}sidebar-submenu-bg: #{$lte-sidebar-submenu-bg-dark}; + --#{$lte-prefix}sidebar-submenu-color: #{$lte-sidebar-submenu-color-dark}; + --#{$lte-prefix}sidebar-submenu-hover-color: #{$lte-sidebar-submenu-hover-color-dark}; + --#{$lte-prefix}sidebar-submenu-hover-bg: #{$lte-sidebar-submenu-hover-bg-dark}; + --#{$lte-prefix}sidebar-submenu-active-color: #{$lte-sidebar-submenu-active-color-dark}; + --#{$lte-prefix}sidebar-submenu-active-bg: #{$lte-sidebar-submenu-active-bg-dark}; + --#{$lte-prefix}sidebar-header-color: #{$lte-sidebar-header-color-dark}; + } + } +} + diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-wrapper.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-wrapper.scss new file mode 100644 index 00000000..5ecc611d --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_app-wrapper.scss @@ -0,0 +1,23 @@ +// +// Core: Layout +// + +.app-wrapper { + position: relative; + display: grid; + grid-template-areas: + "#{$lte-prefix}app-sidebar #{$lte-prefix}app-header" + "#{$lte-prefix}app-sidebar #{$lte-prefix}app-main" + "#{$lte-prefix}app-sidebar #{$lte-prefix}app-footer"; + grid-template-rows: min-content 1fr min-content; + grid-template-columns: auto 1fr; + grid-gap: 0; + align-content: stretch; + align-items: stretch; + max-width: 100vw; + min-height: 100vh; + + > * { + min-width: 0; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_bootstrap-variables.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_bootstrap-variables.scss new file mode 100644 index 00000000..d4c851a9 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_bootstrap-variables.scss @@ -0,0 +1,1766 @@ +// AdminLTE Custom Variables Changes. +// Some of the variables are modified of Bootstrap Original Variables. +// You can find modified variables easily by searching keyword "// adminlte-modified" + +// Variables +// +// Variables should follow the `$component-state-property-size` formula for +// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. + +// Color system + +// scss-docs-start gray-color-variables +$white: #fff !default; +$gray-100: #f8f9fa !default; +$gray-200: #e9ecef !default; +$gray-300: #dee2e6 !default; +$gray-400: #ced4da !default; +$gray-500: #adb5bd !default; +$gray-600: #6c757d !default; +$gray-700: #495057 !default; +$gray-800: #343a40 !default; +$gray-900: #212529 !default; +$black: #000 !default; +// scss-docs-end gray-color-variables + +// fusv-disable +// scss-docs-start gray-colors-map +$grays: ( + "100": $gray-100, + "200": $gray-200, + "300": $gray-300, + "400": $gray-400, + "500": $gray-500, + "600": $gray-600, + "700": $gray-700, + "800": $gray-800, + "900": $gray-900 +) !default; +// scss-docs-end gray-colors-map +// fusv-enable + +// scss-docs-start color-variables +$blue: #0d6efd !default; +$indigo: #6610f2 !default; +$purple: #6f42c1 !default; +$pink: #d63384 !default; +$red: #dc3545 !default; +$orange: #fd7e14 !default; +$yellow: #ffc107 !default; +$green: #198754 !default; +$teal: #20c997 !default; +$cyan: #0dcaf0 !default; +// scss-docs-end color-variables + +// scss-docs-start colors-map +$colors: ( + "blue": $blue, + "indigo": $indigo, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "yellow": $yellow, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "black": $black, + "white": $white, + "gray": $gray-600, + "gray-dark": $gray-800 +) !default; +// scss-docs-end colors-map + +// The contrast ratio to reach against white, to determine if color changes from "light" to "dark". Acceptable values for WCAG 2.2 are 3, 4.5 and 7. +// See https://www.w3.org/TR/WCAG/#contrast-minimum +$min-contrast-ratio: 4.5 !default; + +// Customize the light and dark text colors for use in our color contrast function. +$color-contrast-dark: $black !default; +$color-contrast-light: $white !default; + +// fusv-disable +$blue-100: tint-color($blue, 80%) !default; +$blue-200: tint-color($blue, 60%) !default; +$blue-300: tint-color($blue, 40%) !default; +$blue-400: tint-color($blue, 20%) !default; +$blue-500: $blue !default; +$blue-600: shade-color($blue, 20%) !default; +$blue-700: shade-color($blue, 40%) !default; +$blue-800: shade-color($blue, 60%) !default; +$blue-900: shade-color($blue, 80%) !default; + +$indigo-100: tint-color($indigo, 80%) !default; +$indigo-200: tint-color($indigo, 60%) !default; +$indigo-300: tint-color($indigo, 40%) !default; +$indigo-400: tint-color($indigo, 20%) !default; +$indigo-500: $indigo !default; +$indigo-600: shade-color($indigo, 20%) !default; +$indigo-700: shade-color($indigo, 40%) !default; +$indigo-800: shade-color($indigo, 60%) !default; +$indigo-900: shade-color($indigo, 80%) !default; + +$purple-100: tint-color($purple, 80%) !default; +$purple-200: tint-color($purple, 60%) !default; +$purple-300: tint-color($purple, 40%) !default; +$purple-400: tint-color($purple, 20%) !default; +$purple-500: $purple !default; +$purple-600: shade-color($purple, 20%) !default; +$purple-700: shade-color($purple, 40%) !default; +$purple-800: shade-color($purple, 60%) !default; +$purple-900: shade-color($purple, 80%) !default; + +$pink-100: tint-color($pink, 80%) !default; +$pink-200: tint-color($pink, 60%) !default; +$pink-300: tint-color($pink, 40%) !default; +$pink-400: tint-color($pink, 20%) !default; +$pink-500: $pink !default; +$pink-600: shade-color($pink, 20%) !default; +$pink-700: shade-color($pink, 40%) !default; +$pink-800: shade-color($pink, 60%) !default; +$pink-900: shade-color($pink, 80%) !default; + +$red-100: tint-color($red, 80%) !default; +$red-200: tint-color($red, 60%) !default; +$red-300: tint-color($red, 40%) !default; +$red-400: tint-color($red, 20%) !default; +$red-500: $red !default; +$red-600: shade-color($red, 20%) !default; +$red-700: shade-color($red, 40%) !default; +$red-800: shade-color($red, 60%) !default; +$red-900: shade-color($red, 80%) !default; + +$orange-100: tint-color($orange, 80%) !default; +$orange-200: tint-color($orange, 60%) !default; +$orange-300: tint-color($orange, 40%) !default; +$orange-400: tint-color($orange, 20%) !default; +$orange-500: $orange !default; +$orange-600: shade-color($orange, 20%) !default; +$orange-700: shade-color($orange, 40%) !default; +$orange-800: shade-color($orange, 60%) !default; +$orange-900: shade-color($orange, 80%) !default; + +$yellow-100: tint-color($yellow, 80%) !default; +$yellow-200: tint-color($yellow, 60%) !default; +$yellow-300: tint-color($yellow, 40%) !default; +$yellow-400: tint-color($yellow, 20%) !default; +$yellow-500: $yellow !default; +$yellow-600: shade-color($yellow, 20%) !default; +$yellow-700: shade-color($yellow, 40%) !default; +$yellow-800: shade-color($yellow, 60%) !default; +$yellow-900: shade-color($yellow, 80%) !default; + +$green-100: tint-color($green, 80%) !default; +$green-200: tint-color($green, 60%) !default; +$green-300: tint-color($green, 40%) !default; +$green-400: tint-color($green, 20%) !default; +$green-500: $green !default; +$green-600: shade-color($green, 20%) !default; +$green-700: shade-color($green, 40%) !default; +$green-800: shade-color($green, 60%) !default; +$green-900: shade-color($green, 80%) !default; + +$teal-100: tint-color($teal, 80%) !default; +$teal-200: tint-color($teal, 60%) !default; +$teal-300: tint-color($teal, 40%) !default; +$teal-400: tint-color($teal, 20%) !default; +$teal-500: $teal !default; +$teal-600: shade-color($teal, 20%) !default; +$teal-700: shade-color($teal, 40%) !default; +$teal-800: shade-color($teal, 60%) !default; +$teal-900: shade-color($teal, 80%) !default; + +$cyan-100: tint-color($cyan, 80%) !default; +$cyan-200: tint-color($cyan, 60%) !default; +$cyan-300: tint-color($cyan, 40%) !default; +$cyan-400: tint-color($cyan, 20%) !default; +$cyan-500: $cyan !default; +$cyan-600: shade-color($cyan, 20%) !default; +$cyan-700: shade-color($cyan, 40%) !default; +$cyan-800: shade-color($cyan, 60%) !default; +$cyan-900: shade-color($cyan, 80%) !default; + +$blues: ( + "blue-100": $blue-100, + "blue-200": $blue-200, + "blue-300": $blue-300, + "blue-400": $blue-400, + "blue-500": $blue-500, + "blue-600": $blue-600, + "blue-700": $blue-700, + "blue-800": $blue-800, + "blue-900": $blue-900 +) !default; + +$indigos: ( + "indigo-100": $indigo-100, + "indigo-200": $indigo-200, + "indigo-300": $indigo-300, + "indigo-400": $indigo-400, + "indigo-500": $indigo-500, + "indigo-600": $indigo-600, + "indigo-700": $indigo-700, + "indigo-800": $indigo-800, + "indigo-900": $indigo-900 +) !default; + +$purples: ( + "purple-100": $purple-100, + "purple-200": $purple-200, + "purple-300": $purple-300, + "purple-400": $purple-400, + "purple-500": $purple-500, + "purple-600": $purple-600, + "purple-700": $purple-700, + "purple-800": $purple-800, + "purple-900": $purple-900 +) !default; + +$pinks: ( + "pink-100": $pink-100, + "pink-200": $pink-200, + "pink-300": $pink-300, + "pink-400": $pink-400, + "pink-500": $pink-500, + "pink-600": $pink-600, + "pink-700": $pink-700, + "pink-800": $pink-800, + "pink-900": $pink-900 +) !default; + +$reds: ( + "red-100": $red-100, + "red-200": $red-200, + "red-300": $red-300, + "red-400": $red-400, + "red-500": $red-500, + "red-600": $red-600, + "red-700": $red-700, + "red-800": $red-800, + "red-900": $red-900 +) !default; + +$oranges: ( + "orange-100": $orange-100, + "orange-200": $orange-200, + "orange-300": $orange-300, + "orange-400": $orange-400, + "orange-500": $orange-500, + "orange-600": $orange-600, + "orange-700": $orange-700, + "orange-800": $orange-800, + "orange-900": $orange-900 +) !default; + +$yellows: ( + "yellow-100": $yellow-100, + "yellow-200": $yellow-200, + "yellow-300": $yellow-300, + "yellow-400": $yellow-400, + "yellow-500": $yellow-500, + "yellow-600": $yellow-600, + "yellow-700": $yellow-700, + "yellow-800": $yellow-800, + "yellow-900": $yellow-900 +) !default; + +$greens: ( + "green-100": $green-100, + "green-200": $green-200, + "green-300": $green-300, + "green-400": $green-400, + "green-500": $green-500, + "green-600": $green-600, + "green-700": $green-700, + "green-800": $green-800, + "green-900": $green-900 +) !default; + +$teals: ( + "teal-100": $teal-100, + "teal-200": $teal-200, + "teal-300": $teal-300, + "teal-400": $teal-400, + "teal-500": $teal-500, + "teal-600": $teal-600, + "teal-700": $teal-700, + "teal-800": $teal-800, + "teal-900": $teal-900 +) !default; + +$cyans: ( + "cyan-100": $cyan-100, + "cyan-200": $cyan-200, + "cyan-300": $cyan-300, + "cyan-400": $cyan-400, + "cyan-500": $cyan-500, + "cyan-600": $cyan-600, + "cyan-700": $cyan-700, + "cyan-800": $cyan-800, + "cyan-900": $cyan-900 +) !default; +// fusv-enable + +// scss-docs-start theme-color-variables +$primary: $blue !default; +$secondary: $gray-600 !default; +$success: $green !default; +$info: $cyan !default; +$warning: $yellow !default; +$danger: $red !default; +$light: $gray-100 !default; +$dark: $gray-900 !default; +// scss-docs-end theme-color-variables + +// scss-docs-start theme-colors-map +$theme-colors: ( + "primary": $primary, + "secondary": $secondary, + "success": $success, + "info": $info, + "warning": $warning, + "danger": $danger, + "light": $light, + "dark": $dark +) !default; +// scss-docs-end theme-colors-map + +// scss-docs-start theme-text-variables +$primary-text-emphasis: shade-color($primary, 60%) !default; +$secondary-text-emphasis: shade-color($secondary, 60%) !default; +$success-text-emphasis: shade-color($success, 60%) !default; +$info-text-emphasis: shade-color($info, 60%) !default; +$warning-text-emphasis: shade-color($warning, 60%) !default; +$danger-text-emphasis: shade-color($danger, 60%) !default; +$light-text-emphasis: $gray-700 !default; +$dark-text-emphasis: $gray-700 !default; +// scss-docs-end theme-text-variables + +// scss-docs-start theme-bg-subtle-variables +$primary-bg-subtle: tint-color($primary, 80%) !default; +$secondary-bg-subtle: tint-color($secondary, 80%) !default; +$success-bg-subtle: tint-color($success, 80%) !default; +$info-bg-subtle: tint-color($info, 80%) !default; +$warning-bg-subtle: tint-color($warning, 80%) !default; +$danger-bg-subtle: tint-color($danger, 80%) !default; +$light-bg-subtle: mix($gray-100, $white) !default; +$dark-bg-subtle: $gray-400 !default; +// scss-docs-end theme-bg-subtle-variables + +// scss-docs-start theme-border-subtle-variables +$primary-border-subtle: tint-color($primary, 60%) !default; +$secondary-border-subtle: tint-color($secondary, 60%) !default; +$success-border-subtle: tint-color($success, 60%) !default; +$info-border-subtle: tint-color($info, 60%) !default; +$warning-border-subtle: tint-color($warning, 60%) !default; +$danger-border-subtle: tint-color($danger, 60%) !default; +$light-border-subtle: $gray-200 !default; +$dark-border-subtle: $gray-500 !default; +// scss-docs-end theme-border-subtle-variables + +// Characters which are escaped by the escape-svg function +$escaped-characters: ( + ("<", "%3c"), + (">", "%3e"), + ("#", "%23"), + ("(", "%28"), + (")", "%29"), +) !default; + +// Options +// +// Quickly modify global styling by enabling or disabling optional features. + +$enable-caret: true !default; +$enable-rounded: true !default; +$enable-shadows: true !default; // adminlte-modified +$enable-gradients: false !default; +$enable-transitions: true !default; +$enable-reduced-motion: true !default; +$enable-smooth-scroll: true !default; +$enable-grid-classes: true !default; +$enable-container-classes: true !default; +$enable-cssgrid: false !default; +$enable-button-pointers: true !default; +$enable-rfs: true !default; +$enable-validation-icons: true !default; +$enable-negative-margins: true !default; // adminlte-modified +$enable-deprecation-messages: true !default; +$enable-important-utilities: true !default; + +$enable-dark-mode: true !default; +$color-mode-type: data !default; // `data` or `media-query` + +// Prefix for :root CSS variables + +$variable-prefix: bs- !default; // Deprecated in v5.2.0 for the shorter `$prefix` +$prefix: $variable-prefix !default; + +// Gradient +// +// The gradient which is added to components if `$enable-gradients` is `true` +// This gradient is also added to elements with `.bg-gradient` +// scss-docs-start variable-gradient +$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default; +// scss-docs-end variable-gradient + +// Spacing +// +// Control the default styling of most Bootstrap elements by modifying these +// variables. Mostly focused on spacing. +// You can add more entries to the $spacers map, should you need more variation. + +// scss-docs-start spacer-variables-maps +$spacer: 1rem !default; +$spacers: ( + 0: 0, + 1: $spacer * .25, + 2: $spacer * .5, + 3: $spacer, + 4: $spacer * 1.5, + 5: $spacer * 3, +) !default; +// scss-docs-end spacer-variables-maps + +// Position +// +// Define the edge positioning anchors of the position utilities. + +// scss-docs-start position-map +$position-values: ( + 0: 0, + 50: 50%, + 100: 100% +) !default; +// scss-docs-end position-map + +// Body +// +// Settings for the `` element. + +$body-text-align: null !default; +$body-color: $gray-900 !default; +$body-bg: $white !default; + +$body-secondary-color: rgba($body-color, .75) !default; +$body-secondary-bg: $gray-200 !default; + +$body-tertiary-color: rgba($body-color, .5) !default; +$body-tertiary-bg: $gray-100 !default; + +$body-emphasis-color: $black !default; + +// Links +// +// Style anchor elements. + +$link-color: $primary !default; +$link-decoration: underline !default; +$link-shade-percentage: 20% !default; +$link-hover-color: shift-color($link-color, $link-shade-percentage) !default; +$link-hover-decoration: null !default; + +$stretched-link-pseudo-element: after !default; +$stretched-link-z-index: 1 !default; + +// Icon links +// scss-docs-start icon-link-variables +$icon-link-gap: .375rem !default; +$icon-link-underline-offset: .25em !default; +$icon-link-icon-size: 1em !default; +$icon-link-icon-transition: .2s ease-in-out transform !default; +$icon-link-icon-transform: translate3d(.25em, 0, 0) !default; +// scss-docs-end icon-link-variables + +// Paragraphs +// +// Style p element. + +$paragraph-margin-bottom: 1rem !default; + + +// Grid breakpoints +// +// Define the minimum dimensions at which your layout will change, +// adapting to different screen sizes, for use in media queries. + +// scss-docs-start grid-breakpoints +$grid-breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 992px, + xl: 1200px, + xxl: 1400px +) !default; +// scss-docs-end grid-breakpoints + +@include _assert-ascending($grid-breakpoints, "$grid-breakpoints"); +@include _assert-starts-at-zero($grid-breakpoints, "$grid-breakpoints"); + + +// Grid containers +// +// Define the maximum width of `.container` for different screen sizes. + +// scss-docs-start container-max-widths +$container-max-widths: ( + sm: 540px, + md: 720px, + lg: 960px, + xl: 1140px, + xxl: 1320px +) !default; +// scss-docs-end container-max-widths + +@include _assert-ascending($container-max-widths, "$container-max-widths"); + + +// Grid columns +// +// Set the number of columns and specify the width of the gutters. + +$grid-columns: 12 !default; +$grid-gutter-width: 1.5rem !default; +$grid-row-columns: 6 !default; + +// Container padding + +$container-padding-x: $grid-gutter-width !default; + + +// Components +// +// Define common padding and border radius sizes and more. + +// scss-docs-start border-variables +$border-width: 1px !default; +$border-widths: ( + 1: 1px, + 2: 2px, + 3: 3px, + 4: 4px, + 5: 5px +) !default; +$border-style: solid !default; +$border-color: $gray-300 !default; +$border-color-translucent: rgba($black, .175) !default; +// scss-docs-end border-variables + +// scss-docs-start border-radius-variables +$border-radius: .375rem !default; +$border-radius-sm: .25rem !default; +$border-radius-lg: .5rem !default; +$border-radius-xl: 1rem !default; +$border-radius-xxl: 2rem !default; +$border-radius-pill: 50rem !default; +// scss-docs-end border-radius-variables +// fusv-disable +$border-radius-2xl: $border-radius-xxl !default; // Deprecated in v5.3.0 +// fusv-enable + +// scss-docs-start box-shadow-variables +$box-shadow: 0 .5rem 1rem rgba($black, .15) !default; +$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default; +$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default; +$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default; +// scss-docs-end box-shadow-variables + +$component-active-color: $white !default; +$component-active-bg: $primary !default; + +// scss-docs-start focus-ring-variables +$focus-ring-width: .25rem !default; +$focus-ring-opacity: .25 !default; +$focus-ring-color: rgba($primary, $focus-ring-opacity) !default; +$focus-ring-blur: 0 !default; +$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default; +// scss-docs-end focus-ring-variables + +// scss-docs-start caret-variables +$caret-width: .3em !default; +$caret-vertical-align: $caret-width * .85 !default; +$caret-spacing: $caret-width * .85 !default; +// scss-docs-end caret-variables + +$transition-base: all .2s ease-in-out !default; +$transition-fade: opacity .15s linear !default; +// scss-docs-start collapse-transition +$transition-collapse: height .35s ease !default; +$transition-collapse-width: width .35s ease !default; +// scss-docs-end collapse-transition + +// stylelint-disable function-disallowed-list +// scss-docs-start aspect-ratios +$aspect-ratios: ( + "1x1": 100%, + "4x3": calc(3 / 4 * 100%), + "16x9": calc(9 / 16 * 100%), + "21x9": calc(9 / 21 * 100%) +) !default; +// scss-docs-end aspect-ratios +// stylelint-enable function-disallowed-list + +// Typography +// +// Font, line-height, and color for body text, headings, and more. + +// scss-docs-start font-variables +// stylelint-disable value-keyword-case +$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" !default; // adminlte-modified +$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default; +// stylelint-enable value-keyword-case +$font-family-base: var(--#{$prefix}font-sans-serif) !default; +$font-family-code: var(--#{$prefix}font-monospace) !default; + +// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins +// $font-size-base affects the font size of the body text +$font-size-root: null !default; +$font-size-base: 1rem !default; // Assumes the browser default, typically `16px` +$font-size-sm: $font-size-base * .875 !default; +$font-size-lg: $font-size-base * 1.25 !default; +$font-size-xs: $font-size-base * .75 !default; // adminlte-modified +$font-size-xl: $font-size-base * 2 !default; // adminlte-modified + +$font-weight-lighter: lighter !default; +$font-weight-light: 300 !default; +$font-weight-normal: 400 !default; +$font-weight-medium: 500 !default; +$font-weight-semibold: 600 !default; +$font-weight-bold: 700 !default; +$font-weight-bolder: bolder !default; + +$font-weight-base: $font-weight-normal !default; + +$line-height-base: 1.5 !default; +$line-height-sm: 1.25 !default; +$line-height-lg: 2 !default; + +$h1-font-size: $font-size-base * 2.5 !default; +$h2-font-size: $font-size-base * 2 !default; +$h3-font-size: $font-size-base * 1.75 !default; +$h4-font-size: $font-size-base * 1.5 !default; +$h5-font-size: $font-size-base * 1.25 !default; +$h6-font-size: $font-size-base !default; +$h7-font-size: $font-size-sm !default; // adminlte-modified +$h8-font-size: $font-size-xs !default; // adminlte-modified +// scss-docs-end font-variables + +// scss-docs-start font-sizes +$font-sizes: ( + 1: $h1-font-size, + 2: $h2-font-size, + 3: $h3-font-size, + 4: $h4-font-size, + 5: $h5-font-size, + 6: $h6-font-size, + 7: $h7-font-size, // adminlte-modified + 8: $h8-font-size // adminlte-modified +) !default; +// scss-docs-end font-sizes + +// scss-docs-start headings-variables +$headings-margin-bottom: $spacer * .5 !default; +$headings-font-family: null !default; +$headings-font-style: null !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.2 !default; +$headings-color: inherit !default; +// scss-docs-end headings-variables + +// scss-docs-start display-headings +$display-font-sizes: ( + 1: 5rem, + 2: 4.5rem, + 3: 4rem, + 4: 3.5rem, + 5: 3rem, + 6: 2.5rem +) !default; + +$display-font-family: null !default; +$display-font-style: null !default; +$display-font-weight: 300 !default; +$display-line-height: $headings-line-height !default; +// scss-docs-end display-headings + +// scss-docs-start type-variables +$lead-font-size: $font-size-base * 1.25 !default; +$lead-font-weight: 300 !default; + +$small-font-size: .875em !default; + +$sub-sup-font-size: .75em !default; + +// fusv-disable +$text-muted: var(--#{$prefix}secondary-color) !default; // Deprecated in 5.3.0 +// fusv-enable + +$initialism-font-size: $small-font-size !default; + +$blockquote-margin-y: $spacer !default; +$blockquote-font-size: $font-size-base * 1.25 !default; +$blockquote-footer-color: $gray-600 !default; +$blockquote-footer-font-size: $small-font-size !default; + +$hr-margin-y: $spacer !default; +$hr-color: inherit !default; + +// fusv-disable +$hr-bg-color: null !default; // Deprecated in v5.2.0 +$hr-height: null !default; // Deprecated in v5.2.0 +// fusv-enable + +$hr-border-color: null !default; // Allows for inherited colors +$hr-border-width: var(--#{$prefix}border-width) !default; +$hr-opacity: .25 !default; + +// scss-docs-start vr-variables +$vr-border-width: var(--#{$prefix}border-width) !default; +// scss-docs-end vr-variables + +$legend-margin-bottom: .5rem !default; +$legend-font-size: 1.5rem !default; +$legend-font-weight: null !default; + +$dt-font-weight: $font-weight-bold !default; + +$list-inline-padding: .5rem !default; + +$mark-padding: .1875em !default; +$mark-color: $body-color !default; +$mark-bg: $yellow-100 !default; +// scss-docs-end type-variables + + +// Tables +// +// Customizes the `.table` component with basic values, each used across all table variations. + +// scss-docs-start table-variables +$table-cell-padding-y: .5rem !default; +$table-cell-padding-x: .5rem !default; +$table-cell-padding-y-sm: .25rem !default; +$table-cell-padding-x-sm: .25rem !default; + +$table-cell-vertical-align: top !default; + +$table-color: var(--#{$prefix}emphasis-color) !default; +$table-bg: var(--#{$prefix}body-bg) !default; +$table-accent-bg: transparent !default; + +$table-th-font-weight: null !default; + +$table-striped-color: $table-color !default; +$table-striped-bg-factor: .05 !default; +$table-striped-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-striped-bg-factor) !default; + +$table-active-color: $table-color !default; +$table-active-bg-factor: .1 !default; +$table-active-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-active-bg-factor) !default; + +$table-hover-color: $table-color !default; +$table-hover-bg-factor: .075 !default; +$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default; + +$table-border-factor: .2 !default; +$table-border-width: var(--#{$prefix}border-width) !default; +$table-border-color: var(--#{$prefix}border-color) !default; + +$table-striped-order: odd !default; +$table-striped-columns-order: even !default; + +$table-group-separator-color: currentcolor !default; + +$table-caption-color: var(--#{$prefix}secondary-color) !default; + +$table-bg-scale: -80% !default; +// scss-docs-end table-variables + +// scss-docs-start table-loop +$table-variants: ( + "primary": shift-color($primary, $table-bg-scale), + "secondary": shift-color($secondary, $table-bg-scale), + "success": shift-color($success, $table-bg-scale), + "info": shift-color($info, $table-bg-scale), + "warning": shift-color($warning, $table-bg-scale), + "danger": shift-color($danger, $table-bg-scale), + "light": $light, + "dark": $dark, +) !default; +// scss-docs-end table-loop + + +// Buttons + Forms +// +// Shared variables that are reassigned to `$input-` and `$btn-` specific variables. + +// scss-docs-start input-btn-variables +$input-btn-padding-y: .375rem !default; +$input-btn-padding-x: .75rem !default; +$input-btn-font-family: null !default; +$input-btn-font-size: $font-size-base !default; +$input-btn-line-height: $line-height-base !default; + +$input-btn-focus-width: $focus-ring-width !default; +$input-btn-focus-color-opacity: $focus-ring-opacity !default; +$input-btn-focus-color: $focus-ring-color !default; +$input-btn-focus-blur: $focus-ring-blur !default; +$input-btn-focus-box-shadow: $focus-ring-box-shadow !default; + +$input-btn-padding-y-sm: .25rem !default; +$input-btn-padding-x-sm: .5rem !default; +$input-btn-font-size-sm: $font-size-sm !default; + +$input-btn-padding-y-lg: .5rem !default; +$input-btn-padding-x-lg: 1rem !default; +$input-btn-font-size-lg: $font-size-lg !default; + +$input-btn-border-width: var(--#{$prefix}border-width) !default; +// scss-docs-end input-btn-variables + + +// Buttons +// +// For each of Bootstrap's buttons, define text, background, and border color. + +// scss-docs-start btn-variables +$btn-color: var(--#{$prefix}body-color) !default; +$btn-padding-y: $input-btn-padding-y !default; +$btn-padding-x: $input-btn-padding-x !default; +$btn-font-family: $input-btn-font-family !default; +$btn-font-size: $input-btn-font-size !default; +$btn-line-height: $input-btn-line-height !default; +$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping + +$btn-padding-y-sm: $input-btn-padding-y-sm !default; +$btn-padding-x-sm: $input-btn-padding-x-sm !default; +$btn-font-size-sm: $input-btn-font-size-sm !default; + +$btn-padding-y-lg: $input-btn-padding-y-lg !default; +$btn-padding-x-lg: $input-btn-padding-x-lg !default; +$btn-font-size-lg: $input-btn-font-size-lg !default; + +$btn-border-width: $input-btn-border-width !default; + +$btn-font-weight: $font-weight-normal !default; +$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; +$btn-focus-width: $input-btn-focus-width !default; +$btn-focus-box-shadow: $input-btn-focus-box-shadow !default; +$btn-disabled-opacity: .65 !default; +$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default; + +$btn-link-color: var(--#{$prefix}link-color) !default; +$btn-link-hover-color: var(--#{$prefix}link-hover-color) !default; +$btn-link-disabled-color: $gray-600 !default; +$btn-link-focus-shadow-rgb: to-rgb(mix(color-contrast($link-color), $link-color, 15%)) !default; + +// Allows for customizing button radius independently from global border radius +$btn-border-radius: var(--#{$prefix}border-radius) !default; +$btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; +$btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; + +$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; + +$btn-hover-bg-shade-amount: 15% !default; +$btn-hover-bg-tint-amount: 15% !default; +$btn-hover-border-shade-amount: 20% !default; +$btn-hover-border-tint-amount: 10% !default; +$btn-active-bg-shade-amount: 20% !default; +$btn-active-bg-tint-amount: 20% !default; +$btn-active-border-shade-amount: 25% !default; +$btn-active-border-tint-amount: 10% !default; +// scss-docs-end btn-variables + + +// Forms + +// scss-docs-start form-text-variables +$form-text-margin-top: .25rem !default; +$form-text-font-size: $small-font-size !default; +$form-text-font-style: null !default; +$form-text-font-weight: null !default; +$form-text-color: var(--#{$prefix}secondary-color) !default; +// scss-docs-end form-text-variables + +// scss-docs-start form-label-variables +$form-label-margin-bottom: .5rem !default; +$form-label-font-size: null !default; +$form-label-font-style: null !default; +$form-label-font-weight: null !default; +$form-label-color: null !default; +// scss-docs-end form-label-variables + +// scss-docs-start form-input-variables +$input-padding-y: $input-btn-padding-y !default; +$input-padding-x: $input-btn-padding-x !default; +$input-font-family: $input-btn-font-family !default; +$input-font-size: $input-btn-font-size !default; +$input-font-weight: $font-weight-base !default; +$input-line-height: $input-btn-line-height !default; + +$input-padding-y-sm: $input-btn-padding-y-sm !default; +$input-padding-x-sm: $input-btn-padding-x-sm !default; +$input-font-size-sm: $input-btn-font-size-sm !default; + +$input-padding-y-lg: $input-btn-padding-y-lg !default; +$input-padding-x-lg: $input-btn-padding-x-lg !default; +$input-font-size-lg: $input-btn-font-size-lg !default; + +$input-bg: var(--#{$prefix}body-bg) !default; +$input-disabled-color: null !default; +$input-disabled-bg: var(--#{$prefix}secondary-bg) !default; +$input-disabled-border-color: null !default; + +$input-color: var(--#{$prefix}body-color) !default; +$input-border-color: var(--#{$prefix}border-color) !default; +$input-border-width: $input-btn-border-width !default; +$input-box-shadow: var(--#{$prefix}box-shadow-inset) !default; + +$input-border-radius: var(--#{$prefix}border-radius) !default; +$input-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; +$input-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; + +$input-focus-bg: $input-bg !default; +$input-focus-border-color: tint-color($component-active-bg, 50%) !default; +$input-focus-color: $input-color !default; +$input-focus-width: $input-btn-focus-width !default; +$input-focus-box-shadow: $input-btn-focus-box-shadow !default; + +$input-placeholder-color: var(--#{$prefix}secondary-color) !default; +$input-plaintext-color: var(--#{$prefix}body-color) !default; + +$input-height-border: calc(#{$input-border-width} * 2) !default; // stylelint-disable-line function-disallowed-list + +$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default; +$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default; +$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default; + +$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default; +$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default; +$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default; + +$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; + +$form-color-width: 3rem !default; +// scss-docs-end form-input-variables + +// scss-docs-start form-check-variables +$form-check-input-width: 1em !default; +$form-check-min-height: $font-size-base * $line-height-base !default; +$form-check-padding-start: $form-check-input-width + .5em !default; +$form-check-margin-bottom: .125rem !default; +$form-check-label-color: null !default; +$form-check-label-cursor: null !default; +$form-check-transition: null !default; + +$form-check-input-active-filter: brightness(90%) !default; + +$form-check-input-bg: $input-bg !default; +$form-check-input-border: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color) !default; +$form-check-input-border-radius: .25em !default; +$form-check-radio-border-radius: 50% !default; +$form-check-input-focus-border: $input-focus-border-color !default; +$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default; + +$form-check-input-checked-color: $component-active-color !default; +$form-check-input-checked-bg-color: $component-active-bg !default; +$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default; +$form-check-input-checked-bg-image: url("data:image/svg+xml,") !default; +$form-check-radio-checked-bg-image: url("data:image/svg+xml,") !default; + +$form-check-input-indeterminate-color: $component-active-color !default; +$form-check-input-indeterminate-bg-color: $component-active-bg !default; +$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default; +$form-check-input-indeterminate-bg-image: url("data:image/svg+xml,") !default; + +$form-check-input-disabled-opacity: .5 !default; +$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default; +$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default; + +$form-check-inline-margin-end: 1rem !default; +// scss-docs-end form-check-variables + +// scss-docs-start form-switch-variables +$form-switch-color: rgba($black, .25) !default; +$form-switch-width: 2em !default; +$form-switch-padding-start: $form-switch-width + .5em !default; +$form-switch-bg-image: url("data:image/svg+xml,") !default; +$form-switch-border-radius: $form-switch-width !default; +$form-switch-transition: background-position .15s ease-in-out !default; + +$form-switch-focus-color: $input-focus-border-color !default; +$form-switch-focus-bg-image: url("data:image/svg+xml,") !default; + +$form-switch-checked-color: $component-active-color !default; +$form-switch-checked-bg-image: url("data:image/svg+xml,") !default; +$form-switch-checked-bg-position: right center !default; +// scss-docs-end form-switch-variables + +// scss-docs-start input-group-variables +$input-group-addon-padding-y: $input-padding-y !default; +$input-group-addon-padding-x: $input-padding-x !default; +$input-group-addon-font-weight: $input-font-weight !default; +$input-group-addon-color: $input-color !default; +$input-group-addon-bg: var(--#{$prefix}tertiary-bg) !default; +$input-group-addon-border-color: $input-border-color !default; +// scss-docs-end input-group-variables + +// scss-docs-start form-select-variables +$form-select-padding-y: $input-padding-y !default; +$form-select-padding-x: $input-padding-x !default; +$form-select-font-family: $input-font-family !default; +$form-select-font-size: $input-font-size !default; +$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image +$form-select-font-weight: $input-font-weight !default; +$form-select-line-height: $input-line-height !default; +$form-select-color: $input-color !default; +$form-select-bg: $input-bg !default; +$form-select-disabled-color: null !default; +$form-select-disabled-bg: $input-disabled-bg !default; +$form-select-disabled-border-color: $input-disabled-border-color !default; +$form-select-bg-position: right $form-select-padding-x center !default; +$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions +$form-select-indicator-color: $gray-800 !default; +$form-select-indicator: url("data:image/svg+xml,") !default; + +$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default; +$form-select-feedback-icon-position: center right $form-select-indicator-padding !default; +$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default; + +$form-select-border-width: $input-border-width !default; +$form-select-border-color: $input-border-color !default; +$form-select-border-radius: $input-border-radius !default; +$form-select-box-shadow: var(--#{$prefix}box-shadow-inset) !default; + +$form-select-focus-border-color: $input-focus-border-color !default; +$form-select-focus-width: $input-focus-width !default; +$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default; + +$form-select-padding-y-sm: $input-padding-y-sm !default; +$form-select-padding-x-sm: $input-padding-x-sm !default; +$form-select-font-size-sm: $input-font-size-sm !default; +$form-select-border-radius-sm: $input-border-radius-sm !default; + +$form-select-padding-y-lg: $input-padding-y-lg !default; +$form-select-padding-x-lg: $input-padding-x-lg !default; +$form-select-font-size-lg: $input-font-size-lg !default; +$form-select-border-radius-lg: $input-border-radius-lg !default; + +$form-select-transition: $input-transition !default; +// scss-docs-end form-select-variables + +// scss-docs-start form-range-variables +$form-range-track-width: 100% !default; +$form-range-track-height: .5rem !default; +$form-range-track-cursor: pointer !default; +$form-range-track-bg: var(--#{$prefix}secondary-bg) !default; +$form-range-track-border-radius: 1rem !default; +$form-range-track-box-shadow: var(--#{$prefix}box-shadow-inset) !default; + +$form-range-thumb-width: 1rem !default; +$form-range-thumb-height: $form-range-thumb-width !default; +$form-range-thumb-bg: $component-active-bg !default; +$form-range-thumb-border: 0 !default; +$form-range-thumb-border-radius: 1rem !default; +$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default; +$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default; +$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge +$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default; +$form-range-thumb-disabled-bg: var(--#{$prefix}secondary-color) !default; +$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; +// scss-docs-end form-range-variables + +// scss-docs-start form-file-variables +$form-file-button-color: $input-color !default; +$form-file-button-bg: var(--#{$prefix}tertiary-bg) !default; +$form-file-button-hover-bg: var(--#{$prefix}secondary-bg) !default; +// scss-docs-end form-file-variables + +// scss-docs-start form-floating-variables +$form-floating-height: add(3.5rem, $input-height-border) !default; +$form-floating-line-height: 1.25 !default; +$form-floating-padding-x: $input-padding-x !default; +$form-floating-padding-y: 1rem !default; +$form-floating-input-padding-t: 1.625rem !default; +$form-floating-input-padding-b: .625rem !default; +$form-floating-label-height: 1.5em !default; +$form-floating-label-opacity: .65 !default; +$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default; +$form-floating-label-disabled-color: $gray-600 !default; +$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default; +// scss-docs-end form-floating-variables + +// Form validation + +// scss-docs-start form-feedback-variables +$form-feedback-margin-top: $form-text-margin-top !default; +$form-feedback-font-size: $form-text-font-size !default; +$form-feedback-font-style: $form-text-font-style !default; +$form-feedback-valid-color: $success !default; +$form-feedback-invalid-color: $danger !default; + +$form-feedback-icon-valid-color: $form-feedback-valid-color !default; +$form-feedback-icon-valid: url("data:image/svg+xml,") !default; +$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default; +$form-feedback-icon-invalid: url("data:image/svg+xml,") !default; +// scss-docs-end form-feedback-variables + +// scss-docs-start form-validation-colors +$form-valid-color: $form-feedback-valid-color !default; +$form-valid-border-color: $form-feedback-valid-color !default; +$form-invalid-color: $form-feedback-invalid-color !default; +$form-invalid-border-color: $form-feedback-invalid-color !default; +// scss-docs-end form-validation-colors + +// scss-docs-start form-validation-states +$form-validation-states: ( + "valid": ( + "color": var(--#{$prefix}form-valid-color), + "icon": $form-feedback-icon-valid, + "tooltip-color": #fff, + "tooltip-bg-color": var(--#{$prefix}success), + "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity), + "border-color": var(--#{$prefix}form-valid-border-color), + ), + "invalid": ( + "color": var(--#{$prefix}form-invalid-color), + "icon": $form-feedback-icon-invalid, + "tooltip-color": #fff, + "tooltip-bg-color": var(--#{$prefix}danger), + "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity), + "border-color": var(--#{$prefix}form-invalid-border-color), + ) +) !default; +// scss-docs-end form-validation-states + +// Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. + +// scss-docs-start zindex-stack +$zindex-dropdown: 1000 !default; +$zindex-sticky: 1020 !default; +$zindex-fixed: 1030 !default; +$zindex-offcanvas-backdrop: 1040 !default; +$zindex-offcanvas: 1045 !default; +$zindex-modal-backdrop: 1050 !default; +$zindex-modal: 1055 !default; +$zindex-popover: 1070 !default; +$zindex-tooltip: 1080 !default; +$zindex-toast: 1090 !default; +// scss-docs-end zindex-stack + +// scss-docs-start zindex-levels-map +$zindex-levels: ( + n1: -1, + 0: 0, + 1: 1, + 2: 2, + 3: 3 +) !default; +// scss-docs-end zindex-levels-map + + +// Navs + +// scss-docs-start nav-variables +$nav-link-padding-y: .5rem !default; +$nav-link-padding-x: 1rem !default; +$nav-link-font-size: null !default; +$nav-link-font-weight: null !default; +$nav-link-color: var(--#{$prefix}link-color) !default; +$nav-link-hover-color: var(--#{$prefix}link-hover-color) !default; +$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default; +$nav-link-disabled-color: var(--#{$prefix}secondary-color) !default; +$nav-link-focus-box-shadow: $focus-ring-box-shadow !default; + +$nav-tabs-border-color: var(--#{$prefix}border-color) !default; +$nav-tabs-border-width: var(--#{$prefix}border-width) !default; +$nav-tabs-border-radius: var(--#{$prefix}border-radius) !default; +$nav-tabs-link-hover-border-color: var(--#{$prefix}secondary-bg) var(--#{$prefix}secondary-bg) $nav-tabs-border-color !default; +$nav-tabs-link-active-color: var(--#{$prefix}emphasis-color) !default; +$nav-tabs-link-active-bg: var(--#{$prefix}body-bg) !default; +$nav-tabs-link-active-border-color: var(--#{$prefix}border-color) var(--#{$prefix}border-color) $nav-tabs-link-active-bg !default; + +$nav-pills-border-radius: var(--#{$prefix}border-radius) !default; +$nav-pills-link-active-color: $component-active-color !default; +$nav-pills-link-active-bg: $component-active-bg !default; + +$nav-underline-gap: 1rem !default; +$nav-underline-border-width: .125rem !default; +$nav-underline-link-active-color: var(--#{$prefix}emphasis-color) !default; +// scss-docs-end nav-variables + + +// Navbar + +// scss-docs-start navbar-variables +$navbar-padding-y: $spacer * .5 !default; +$navbar-padding-x: null !default; + +$navbar-nav-link-padding-x: 1rem !default; // adminlte-modified + +$navbar-brand-font-size: $font-size-lg !default; +// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link +$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default; +$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default; +$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default; +$navbar-brand-margin-end: 1rem !default; + +$navbar-toggler-padding-y: .25rem !default; +$navbar-toggler-padding-x: .75rem !default; +$navbar-toggler-font-size: $font-size-lg !default; +$navbar-toggler-border-radius: $btn-border-radius !default; +$navbar-toggler-focus-width: $btn-focus-width !default; +$navbar-toggler-transition: box-shadow .15s ease-in-out !default; + +$navbar-light-color: rgba(var(--#{$prefix}emphasis-color-rgb), .65) !default; +$navbar-light-hover-color: rgba(var(--#{$prefix}emphasis-color-rgb), .8) !default; +$navbar-light-active-color: rgba(var(--#{$prefix}emphasis-color-rgb), 1) !default; +$navbar-light-disabled-color: rgba(var(--#{$prefix}emphasis-color-rgb), .3) !default; +$navbar-light-icon-color: rgba($body-color, .75) !default; +$navbar-light-toggler-icon-bg: url("data:image/svg+xml,") !default; +$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), .15) !default; +$navbar-light-brand-color: $navbar-light-active-color !default; +$navbar-light-brand-hover-color: $navbar-light-active-color !default; +// scss-docs-end navbar-variables + +// scss-docs-start navbar-dark-variables +$navbar-dark-color: rgba($white, .55) !default; +$navbar-dark-hover-color: rgba($white, .75) !default; +$navbar-dark-active-color: $white !default; +$navbar-dark-disabled-color: rgba($white, .25) !default; +$navbar-dark-icon-color: $navbar-dark-color !default; +$navbar-dark-toggler-icon-bg: url("data:image/svg+xml,") !default; +$navbar-dark-toggler-border-color: rgba($white, .1) !default; +$navbar-dark-brand-color: $navbar-dark-active-color !default; +$navbar-dark-brand-hover-color: $navbar-dark-active-color !default; +// scss-docs-end navbar-dark-variables + + +// Dropdowns +// +// Dropdown menu container and contents. + +// scss-docs-start dropdown-variables +$dropdown-min-width: 10rem !default; +$dropdown-padding-x: 0 !default; +$dropdown-padding-y: .5rem !default; +$dropdown-spacer: .125rem !default; +$dropdown-font-size: $font-size-base !default; +$dropdown-color: var(--#{$prefix}body-color) !default; +$dropdown-bg: var(--#{$prefix}body-bg) !default; +$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default; +$dropdown-border-radius: var(--#{$prefix}border-radius) !default; +$dropdown-border-width: var(--#{$prefix}border-width) !default; +$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default; // stylelint-disable-line function-disallowed-list +$dropdown-divider-bg: $dropdown-border-color !default; +$dropdown-divider-margin-y: $spacer * .5 !default; +$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default; + +$dropdown-link-color: var(--#{$prefix}body-color) !default; +$dropdown-link-hover-color: $dropdown-link-color !default; +$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default; + +$dropdown-link-active-color: $component-active-color !default; +$dropdown-link-active-bg: $component-active-bg !default; + +$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default; + +$dropdown-item-padding-y: $spacer * .25 !default; +$dropdown-item-padding-x: $spacer !default; + +$dropdown-header-color: $gray-600 !default; +$dropdown-header-padding-x: $dropdown-item-padding-x !default; +$dropdown-header-padding-y: $dropdown-padding-y !default; +// fusv-disable +$dropdown-header-padding: $dropdown-header-padding-y $dropdown-header-padding-x !default; // Deprecated in v5.2.0 +// fusv-enable +// scss-docs-end dropdown-variables + +// scss-docs-start dropdown-dark-variables +$dropdown-dark-color: $gray-300 !default; +$dropdown-dark-bg: $gray-800 !default; +$dropdown-dark-border-color: $dropdown-border-color !default; +$dropdown-dark-divider-bg: $dropdown-divider-bg !default; +$dropdown-dark-box-shadow: null !default; +$dropdown-dark-link-color: $dropdown-dark-color !default; +$dropdown-dark-link-hover-color: $white !default; +$dropdown-dark-link-hover-bg: rgba($white, .15) !default; +$dropdown-dark-link-active-color: $dropdown-link-active-color !default; +$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default; +$dropdown-dark-link-disabled-color: $gray-500 !default; +$dropdown-dark-header-color: $gray-500 !default; +// scss-docs-end dropdown-dark-variables + + +// Pagination + +// scss-docs-start pagination-variables +$pagination-padding-y: .375rem !default; +$pagination-padding-x: .75rem !default; +$pagination-padding-y-sm: .25rem !default; +$pagination-padding-x-sm: .5rem !default; +$pagination-padding-y-lg: .75rem !default; +$pagination-padding-x-lg: 1.5rem !default; + +$pagination-font-size: $font-size-base !default; + +$pagination-color: var(--#{$prefix}link-color) !default; +$pagination-bg: var(--#{$prefix}body-bg) !default; +$pagination-border-radius: var(--#{$prefix}border-radius) !default; +$pagination-border-width: var(--#{$prefix}border-width) !default; +$pagination-margin-start: calc(-1 * #{$pagination-border-width}) !default; // stylelint-disable-line function-disallowed-list +$pagination-border-color: var(--#{$prefix}border-color) !default; + +$pagination-focus-color: var(--#{$prefix}link-hover-color) !default; +$pagination-focus-bg: var(--#{$prefix}secondary-bg) !default; +$pagination-focus-box-shadow: $focus-ring-box-shadow !default; +$pagination-focus-outline: 0 !default; + +$pagination-hover-color: var(--#{$prefix}link-hover-color) !default; +$pagination-hover-bg: var(--#{$prefix}tertiary-bg) !default; +$pagination-hover-border-color: var(--#{$prefix}border-color) !default; // Todo in v6: remove this? + +$pagination-active-color: $component-active-color !default; +$pagination-active-bg: $component-active-bg !default; +$pagination-active-border-color: $component-active-bg !default; + +$pagination-disabled-color: var(--#{$prefix}secondary-color) !default; +$pagination-disabled-bg: var(--#{$prefix}secondary-bg) !default; +$pagination-disabled-border-color: var(--#{$prefix}border-color) !default; + +$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; + +$pagination-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; +$pagination-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; +// scss-docs-end pagination-variables + + +// Placeholders + +// scss-docs-start placeholders +$placeholder-opacity-max: .5 !default; +$placeholder-opacity-min: .2 !default; +// scss-docs-end placeholders + +// Cards + +// scss-docs-start card-variables +$card-spacer-y: $spacer !default; +$card-spacer-x: $spacer !default; +$card-title-spacer-y: $spacer * .5 !default; +$card-title-color: null !default; +$card-subtitle-color: null !default; +$card-border-width: var(--#{$prefix}border-width) !default; +$card-border-color: var(--#{$prefix}border-color-translucent) !default; +$card-border-radius: var(--#{$prefix}border-radius) !default; +$card-box-shadow: null !default; +$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default; +$card-cap-padding-y: $card-spacer-y * .5 !default; +$card-cap-padding-x: $card-spacer-x !default; +$card-cap-bg: rgba(var(--#{$prefix}body-color-rgb), .03) !default; +$card-cap-color: null !default; +$card-height: null !default; +$card-color: null !default; +$card-bg: var(--#{$prefix}body-bg) !default; +$card-img-overlay-padding: $spacer !default; +$card-group-margin: $grid-gutter-width * .5 !default; +// scss-docs-end card-variables + +// Accordion + +// scss-docs-start accordion-variables +$accordion-padding-y: 1rem !default; +$accordion-padding-x: 1.25rem !default; +$accordion-color: var(--#{$prefix}body-color) !default; +$accordion-bg: var(--#{$prefix}body-bg) !default; +$accordion-border-width: var(--#{$prefix}border-width) !default; +$accordion-border-color: var(--#{$prefix}border-color) !default; +$accordion-border-radius: var(--#{$prefix}border-radius) !default; +$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default; + +$accordion-body-padding-y: $accordion-padding-y !default; +$accordion-body-padding-x: $accordion-padding-x !default; + +$accordion-button-padding-y: $accordion-padding-y !default; +$accordion-button-padding-x: $accordion-padding-x !default; +$accordion-button-color: var(--#{$prefix}body-color) !default; +$accordion-button-bg: var(--#{$prefix}accordion-bg) !default; +$accordion-transition: $btn-transition, border-radius .15s ease !default; +$accordion-button-active-bg: var(--#{$prefix}primary-bg-subtle) !default; +$accordion-button-active-color: var(--#{$prefix}primary-text-emphasis) !default; + +// fusv-disable +$accordion-button-focus-border-color: $input-focus-border-color !default; // Deprecated in v5.3.3 +// fusv-enable +$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default; + +$accordion-icon-width: 1.25rem !default; +$accordion-icon-color: $body-color !default; +$accordion-icon-active-color: $primary-text-emphasis !default; +$accordion-icon-transition: transform .2s ease-in-out !default; +$accordion-icon-transform: rotate(-180deg) !default; + +$accordion-button-icon: url("data:image/svg+xml,") !default; +$accordion-button-active-icon: url("data:image/svg+xml,") !default; +// scss-docs-end accordion-variables + +// Tooltips + +// scss-docs-start tooltip-variables +$tooltip-font-size: $font-size-sm !default; +$tooltip-max-width: 200px !default; +$tooltip-color: var(--#{$prefix}body-bg) !default; +$tooltip-bg: var(--#{$prefix}emphasis-color) !default; +$tooltip-border-radius: var(--#{$prefix}border-radius) !default; +$tooltip-opacity: .9 !default; +$tooltip-padding-y: $spacer * .25 !default; +$tooltip-padding-x: $spacer * .5 !default; +$tooltip-margin: null !default; // TODO: remove this in v6 + +$tooltip-arrow-width: .8rem !default; +$tooltip-arrow-height: .4rem !default; +// fusv-disable +$tooltip-arrow-color: null !default; // Deprecated in Bootstrap 5.2.0 for CSS variables +// fusv-enable +// scss-docs-end tooltip-variables + +// Form tooltips must come after regular tooltips +// scss-docs-start tooltip-feedback-variables +$form-feedback-tooltip-padding-y: $tooltip-padding-y !default; +$form-feedback-tooltip-padding-x: $tooltip-padding-x !default; +$form-feedback-tooltip-font-size: $tooltip-font-size !default; +$form-feedback-tooltip-line-height: null !default; +$form-feedback-tooltip-opacity: $tooltip-opacity !default; +$form-feedback-tooltip-border-radius: $tooltip-border-radius !default; +// scss-docs-end tooltip-feedback-variables + + +// Popovers + +// scss-docs-start popover-variables +$popover-font-size: $font-size-sm !default; +$popover-bg: var(--#{$prefix}body-bg) !default; +$popover-max-width: 276px !default; +$popover-border-width: var(--#{$prefix}border-width) !default; +$popover-border-color: var(--#{$prefix}border-color-translucent) !default; +$popover-border-radius: var(--#{$prefix}border-radius-lg) !default; +$popover-inner-border-radius: calc(#{$popover-border-radius} - #{$popover-border-width}) !default; // stylelint-disable-line function-disallowed-list +$popover-box-shadow: var(--#{$prefix}box-shadow) !default; + +$popover-header-font-size: $font-size-base !default; +$popover-header-bg: var(--#{$prefix}secondary-bg) !default; +$popover-header-color: $headings-color !default; +$popover-header-padding-y: .5rem !default; +$popover-header-padding-x: $spacer !default; + +$popover-body-color: var(--#{$prefix}body-color) !default; +$popover-body-padding-y: $spacer !default; +$popover-body-padding-x: $spacer !default; + +$popover-arrow-width: 1rem !default; +$popover-arrow-height: .5rem !default; +// scss-docs-end popover-variables + +// fusv-disable +// Deprecated in Bootstrap 5.2.0 for CSS variables +$popover-arrow-color: $popover-bg !default; +$popover-arrow-outer-color: var(--#{$prefix}border-color-translucent) !default; +// fusv-enable + + +// Toasts + +// scss-docs-start toast-variables +$toast-max-width: 350px !default; +$toast-padding-x: .75rem !default; +$toast-padding-y: .5rem !default; +$toast-font-size: .875rem !default; +$toast-color: null !default; +$toast-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; +$toast-border-width: var(--#{$prefix}border-width) !default; +$toast-border-color: var(--#{$prefix}border-color-translucent) !default; +$toast-border-radius: var(--#{$prefix}border-radius) !default; +$toast-box-shadow: var(--#{$prefix}box-shadow) !default; +$toast-spacing: $container-padding-x !default; + +$toast-header-color: var(--#{$prefix}secondary-color) !default; +$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; +$toast-header-border-color: $toast-border-color !default; +// scss-docs-end toast-variables + + +// Badges + +// scss-docs-start badge-variables +$badge-font-size: .75em !default; +$badge-font-weight: $font-weight-bold !default; +$badge-color: $white !default; +$badge-padding-y: .35em !default; +$badge-padding-x: .65em !default; +$badge-border-radius: var(--#{$prefix}border-radius) !default; +// scss-docs-end badge-variables + + +// Modals + +// scss-docs-start modal-variables +$modal-inner-padding: $spacer !default; + +$modal-footer-margin-between: .5rem !default; + +$modal-dialog-margin: .5rem !default; +$modal-dialog-margin-y-sm-up: 1.75rem !default; + +$modal-title-line-height: $line-height-base !default; + +// AdminLTE keeps `$modal-content-color: null` so the modal text colour is +// inherited from the document. Bootstrap's default is `var(--#{$prefix}body-color)`. +$modal-content-color: null !default; +$modal-content-bg: var(--#{$prefix}body-bg) !default; +$modal-content-border-color: var(--#{$prefix}border-color-translucent) !default; +$modal-content-border-width: var(--#{$prefix}border-width) !default; +$modal-content-border-radius: var(--#{$prefix}border-radius-lg) !default; +$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default; +$modal-content-box-shadow-xs: var(--#{$prefix}box-shadow-sm) !default; +$modal-content-box-shadow-sm-up: var(--#{$prefix}box-shadow) !default; + +$modal-backdrop-bg: $black !default; +$modal-backdrop-opacity: .5 !default; + +$modal-header-border-color: var(--#{$prefix}border-color) !default; +$modal-header-border-width: $modal-content-border-width !default; +$modal-header-padding-y: $modal-inner-padding !default; +$modal-header-padding-x: $modal-inner-padding !default; +$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility + +$modal-footer-bg: null !default; +$modal-footer-border-color: $modal-header-border-color !default; +$modal-footer-border-width: $modal-header-border-width !default; + +$modal-sm: 300px !default; +$modal-md: 500px !default; +$modal-lg: 800px !default; +$modal-xl: 1140px !default; + +$modal-fade-transform: translate(0, -50px) !default; +$modal-show-transform: none !default; +$modal-transition: transform .3s ease-out !default; +$modal-scale-transform: scale(1.02) !default; +// scss-docs-end modal-variables + + +// Alerts +// +// Define alert colors, border radius, and padding. + +// scss-docs-start alert-variables +$alert-padding-y: $spacer !default; +$alert-padding-x: $spacer !default; +$alert-margin-bottom: 1rem !default; +$alert-border-radius: var(--#{$prefix}border-radius) !default; +$alert-link-font-weight: $font-weight-bold !default; +$alert-border-width: var(--#{$prefix}border-width) !default; +$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side +// scss-docs-end alert-variables + +// fusv-disable +$alert-bg-scale: -80% !default; // Deprecated in v5.2.0, to be removed in v6 +$alert-border-scale: -70% !default; // Deprecated in v5.2.0, to be removed in v6 +$alert-color-scale: 40% !default; // Deprecated in v5.2.0, to be removed in v6 +// fusv-enable + +// Progress bars + +// scss-docs-start progress-variables +$progress-height: 1rem !default; +$progress-font-size: $font-size-base * .75 !default; +$progress-bg: var(--#{$prefix}secondary-bg) !default; +$progress-border-radius: var(--#{$prefix}border-radius) !default; +$progress-box-shadow: var(--#{$prefix}box-shadow-inset) !default; +$progress-bar-color: $white !default; +$progress-bar-bg: $primary !default; +$progress-bar-animation-timing: 1s linear infinite !default; +$progress-bar-transition: width .6s ease !default; +// scss-docs-end progress-variables + + +// List group + +// scss-docs-start list-group-variables +$list-group-color: var(--#{$prefix}body-color) !default; +$list-group-bg: var(--#{$prefix}body-bg) !default; +$list-group-border-color: var(--#{$prefix}border-color) !default; +$list-group-border-width: var(--#{$prefix}border-width) !default; +$list-group-border-radius: var(--#{$prefix}border-radius) !default; + +$list-group-item-padding-y: $spacer * .5 !default; +$list-group-item-padding-x: $spacer !default; +// fusv-disable +$list-group-item-bg-scale: -80% !default; // Deprecated in v5.3.0 +$list-group-item-color-scale: 40% !default; // Deprecated in v5.3.0 +// fusv-enable + +$list-group-hover-bg: var(--#{$prefix}tertiary-bg) !default; +$list-group-active-color: $component-active-color !default; +$list-group-active-bg: $component-active-bg !default; +$list-group-active-border-color: $list-group-active-bg !default; + +$list-group-disabled-color: var(--#{$prefix}secondary-color) !default; +$list-group-disabled-bg: $list-group-bg !default; + +$list-group-action-color: var(--#{$prefix}secondary-color) !default; +$list-group-action-hover-color: var(--#{$prefix}emphasis-color) !default; + +$list-group-action-active-color: var(--#{$prefix}body-color) !default; +$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default; +// scss-docs-end list-group-variables + + +// Image thumbnails + +// scss-docs-start thumbnail-variables +$thumbnail-padding: .25rem !default; +$thumbnail-bg: var(--#{$prefix}body-bg) !default; +$thumbnail-border-width: var(--#{$prefix}border-width) !default; +$thumbnail-border-color: var(--#{$prefix}border-color) !default; +$thumbnail-border-radius: var(--#{$prefix}border-radius) !default; +$thumbnail-box-shadow: var(--#{$prefix}box-shadow-sm) !default; +// scss-docs-end thumbnail-variables + + +// Figures + +// scss-docs-start figure-variables +$figure-caption-font-size: $small-font-size !default; +$figure-caption-color: var(--#{$prefix}secondary-color) !default; +// scss-docs-end figure-variables + + +// Breadcrumbs + +// scss-docs-start breadcrumb-variables +$breadcrumb-font-size: null !default; +$breadcrumb-padding-y: 0 !default; +$breadcrumb-padding-x: 0 !default; +$breadcrumb-item-padding-x: .5rem !default; +$breadcrumb-margin-bottom: 1rem !default; +$breadcrumb-bg: null !default; +$breadcrumb-divider-color: var(--#{$prefix}secondary-color) !default; +$breadcrumb-active-color: var(--#{$prefix}secondary-color) !default; +$breadcrumb-divider: quote("/") !default; +$breadcrumb-divider-flipped: $breadcrumb-divider !default; +$breadcrumb-border-radius: null !default; +// scss-docs-end breadcrumb-variables + +// Carousel + +// scss-docs-start carousel-variables +$carousel-control-color: $white !default; +$carousel-control-width: 15% !default; +$carousel-control-opacity: .5 !default; +$carousel-control-hover-opacity: .9 !default; +$carousel-control-transition: opacity .15s ease !default; + +$carousel-indicator-width: 30px !default; +$carousel-indicator-height: 3px !default; +$carousel-indicator-hit-area-height: 10px !default; +$carousel-indicator-spacer: 3px !default; +$carousel-indicator-opacity: .5 !default; +$carousel-indicator-active-bg: $white !default; +$carousel-indicator-active-opacity: 1 !default; +$carousel-indicator-transition: opacity .6s ease !default; + +$carousel-caption-width: 70% !default; +$carousel-caption-color: $white !default; +$carousel-caption-padding-y: 1.25rem !default; +$carousel-caption-spacer: 1.25rem !default; + +$carousel-control-icon-width: 2rem !default; + +$carousel-control-prev-icon-bg: url("data:image/svg+xml,") !default; +$carousel-control-next-icon-bg: url("data:image/svg+xml,") !default; + +$carousel-transition-duration: .6s !default; +$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`) +$carousel-control-icon-filter: null !default; +// scss-docs-end carousel-variables + +// scss-docs-start carousel-dark-variables +// Carousel "dark" variables below have been deprecated in Bootstrap 5.3.4. +// Use the `.carousel` colour-mode rules and `$carousel-control-icon-filter` +// instead. +$carousel-dark-indicator-active-bg: $black !default; // Deprecated in 5.3.4 +$carousel-dark-caption-color: $black !default; // Deprecated in 5.3.4 +$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default; // Deprecated in 5.3.4 +// scss-docs-end carousel-dark-variables + + +// Spinners + +// scss-docs-start spinner-variables +$spinner-width: 2rem !default; +$spinner-height: $spinner-width !default; +$spinner-vertical-align: -.125em !default; +$spinner-border-width: .25em !default; +$spinner-animation-speed: .75s !default; + +$spinner-width-sm: 1rem !default; +$spinner-height-sm: $spinner-width-sm !default; +$spinner-border-width-sm: .2em !default; +// scss-docs-end spinner-variables + + +// Close + +// scss-docs-start close-variables +$btn-close-width: 1em !default; +$btn-close-height: $btn-close-width !default; +$btn-close-padding-x: .25em !default; +$btn-close-padding-y: $btn-close-padding-x !default; +$btn-close-color: $black !default; +$btn-close-bg: url("data:image/svg+xml,") !default; +$btn-close-focus-shadow: $focus-ring-box-shadow !default; +$btn-close-opacity: .5 !default; +$btn-close-hover-opacity: .75 !default; +$btn-close-focus-opacity: 1 !default; +$btn-close-disabled-opacity: .25 !default; +$btn-close-filter: null !default; +$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default; // Deprecated in 5.3.4. Use `$btn-close-filter` with the `.btn-close` colour-mode rules instead. +// scss-docs-end close-variables + + +// Offcanvas + +// scss-docs-start offcanvas-variables +$offcanvas-padding-y: $modal-inner-padding !default; +$offcanvas-padding-x: $modal-inner-padding !default; +$offcanvas-horizontal-width: 400px !default; +$offcanvas-vertical-height: 30vh !default; +$offcanvas-transition-duration: .3s !default; +$offcanvas-border-color: $modal-content-border-color !default; +$offcanvas-border-width: $modal-content-border-width !default; +$offcanvas-title-line-height: $modal-title-line-height !default; +$offcanvas-bg-color: var(--#{$prefix}body-bg) !default; +$offcanvas-color: var(--#{$prefix}body-color) !default; +$offcanvas-box-shadow: $modal-content-box-shadow-xs !default; +$offcanvas-backdrop-bg: $modal-backdrop-bg !default; +$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default; +// scss-docs-end offcanvas-variables + +// Code + +$code-font-size: $small-font-size !default; +$code-color: $pink !default; + +$kbd-padding-y: .1875rem !default; +$kbd-padding-x: .375rem !default; +$kbd-font-size: $code-font-size !default; +$kbd-color: var(--#{$prefix}body-bg) !default; +$kbd-bg: var(--#{$prefix}body-color) !default; +$nested-kbd-font-weight: null !default; // Deprecated in v5.2.0, removing in v6 + +$pre-color: null !default; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_callouts.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_callouts.scss new file mode 100644 index 00000000..21302345 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_callouts.scss @@ -0,0 +1,40 @@ +// +// Callouts +// + +.callout { + --#{$prefix}link-color-rgb: var(--#{$lte-prefix}callout-link); + --#{$prefix}code-color: var(--#{$lte-prefix}callout-code-color); + + padding: 1.25rem; + color: var(--#{$lte-prefix}callout-color, inherit); + background-color: var(--#{$lte-prefix}callout-bg, var(--bs-gray-100)); + border-left: .25rem solid var(--#{$lte-prefix}callout-border, var(--bs-gray-300)); + + .callout-link { + font-weight: $lte-callout-link-font-weight; + color: var(--#{$prefix}callout-link-color); + } + + h4 { + margin-bottom: .25rem; + } + + > :last-child { + margin-bottom: 0; + } + + + .callout { + margin-top: -.25rem; + } +} + +// Variations +@each $name, $color in $theme-colors { + .callout-#{$name} { + --#{$lte-prefix}callout-color: var(--#{$prefix}#{$name}-text-emphasis); + --#{$lte-prefix}callout-bg: var(--#{$prefix}#{$name}-bg-subtle); + --#{$lte-prefix}callout-border: var(--#{$prefix}#{$name}-border-subtle); + --#{$prefix}callout-link-color: var(--#{$prefix}#{$name}-text-emphasis); + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_cards.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_cards.scss new file mode 100644 index 00000000..6d7fc3ab --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_cards.scss @@ -0,0 +1,311 @@ +// +// Component: Cards +// + +// Color variants +.card { + @include box-shadow($lte-card-shadow); + + &[class*="card-"]:not(.card-outline), + &[class*="text-bg-"]:not(.card-outline) { + > .card-header { + color: var(--#{$lte-prefix}card-variant-color); + background-color: var(--#{$lte-prefix}card-variant-bg); + + .btn-tool { + --#{$prefix}btn-color: rgba(var(--#{$lte-prefix}card-variant-color-rgb), .8); + --#{$prefix}btn-hover-color: var(--#{$lte-prefix}card-variant-color); + } + } + } + + &.card-outline { + border-top: 3px solid var(--#{$lte-prefix}card-variant-bg); + } + + &.maximized-card { + position: fixed; + top: 0; + left: 0; + z-index: $zindex-modal-backdrop; + width: 100% !important; + max-width: 100% !important; + height: 100% !important; + max-height: 100% !important; + + &.was-collapsed .card-body { + display: block !important; + } + + .card-body { + overflow: auto; + } + + [data-lte-toggle="card-collapse"] { + display: none; + } + + [data-lte-icon="maximize"] { + display: none; + } + + .card-header, + .card-footer { + @include border-radius(0 !important); + } + } + + &:not(.maximized-card) { + [data-lte-icon="minimize"] { + display: none; + } + } + + // collapsed mode + &.collapsed-card { + // Use > .card-header to scope to direct card only, not nested cards + > .card-header [data-lte-icon="collapse"] { + display: none; + } + + > .card-body, + > .card-footer { + display: none; + } + } + + &:not(.collapsed-card) { + // Use > .card-header to scope to direct card only, not nested cards + > .card-header [data-lte-icon="expand"] { + display: none; + } + } + + + .nav.flex-column { + > li { + margin: 0; + border-bottom: 1px solid $card-border-color; + + &:last-of-type { + border-bottom: 0; + } + } + } + + // fixed height to 300px + &.height-control { + .card-body { + max-height: 300px; + overflow: auto; + } + } + + .border-end { + border-right: 1px solid $card-border-color; + } + + .border-start { + border-left: 1px solid $card-border-color; + } + + &.card-tabs { + &:not(.card-outline) { + > .card-header { + border-bottom: 0; + + .nav-item { + &:first-child .nav-link { + border-left-color: transparent; + } + } + } + } + + &.card-outline { + .nav-item { + border-bottom: 0; + + &:first-child .nav-link { + margin-left: 0; + border-left: 0; + } + } + } + + .card-tools { + margin: .3rem .5rem; + } + + &:not(.expanding-card).collapsed-card { + .card-header { + border-bottom: 0; + + .nav-tabs { + border-bottom: 0; + + .nav-item { + margin-bottom: 0; + } + } + } + } + + &.expanding-card { + .card-header { + .nav-tabs { + .nav-item { + margin-bottom: -1px; + } + } + } + } + } + + &.card-outline-tabs { + border-top: 0; + + .card-header { + .nav-item { + &:first-child .nav-link { + margin-left: 0; + border-left: 0; + } + } + + a { + text-decoration: none; + border-top: 3px solid transparent; + + &:hover { + border-top: 3px solid $nav-tabs-border-color; + } + + &.active { + &:hover { + margin-top: 0; + } + } + } + } + + .card-tools { + margin: .5rem .5rem .3rem; + } + + &:not(.expanding-card).collapsed-card .card-header { + border-bottom: 0; + + .nav-tabs { + border-bottom: 0; + + .nav-item { + margin-bottom: 0; + } + } + } + + &.expanding-card { + .card-header { + .nav-tabs { + .nav-item { + margin-bottom: -1px; + } + } + } + } + } + +} + +// Maximized Card Body Scroll fix +html.maximized-card { + overflow: hidden; +} + +// Add clearfix to header, body and footer +.card-header, +.card-body, +.card-footer { + @include clearfix(); +} + +// Box header +.card-header { + position: relative; + padding: (($card-spacer-y * .5) * 2) $card-spacer-x; + background-color: transparent; + border-bottom: 1px solid $card-border-color; + + @if $enable-rounded { + @include border-top-radius($border-radius); + } + + .collapsed-card & { + border-bottom: 0; + } + + > .card-tools { + float: right; + margin-right: -$card-spacer-x * .5; + + .input-group, + .nav, + .pagination { + margin-top: -$card-spacer-y * .4; + margin-bottom: -$card-spacer-y * .4; + } + + [data-bs-toggle="tooltip"] { + position: relative; + } + } +} + +.card-title { + float: left; + margin: 0; + font-size: $lte-card-title-font-size; + font-weight: $lte-card-title-font-weight; +} + +// Box Tools Buttons +.btn-tool { + --#{$prefix}btn-padding-x: .5rem; + --#{$prefix}btn-padding-y: .25rem; + + &:not(.btn-tool-custom) { + --#{$prefix}btn-color: var(--#{$prefix}tertiary-color); + --#{$prefix}btn-bg: transparent; + --#{$prefix}btn-box-shadow: none; + --#{$prefix}btn-hover-color: var(--#{$prefix}secondary-color); + --#{$prefix}btn-active-border-color: transparent; + } + + margin: -$card-spacer-y 0; + font-size: $font-size-sm; +} + +@each $name, $color in $theme-colors { + .card-#{$name}, + .bg-#{$name}, + .text-bg-#{$name} { + --#{$lte-prefix}card-variant-bg: #{$color}; + --#{$lte-prefix}card-variant-bg-rgb: #{to-rgb($color)}; + --#{$lte-prefix}card-variant-color: #{color-contrast($color)}; + --#{$lte-prefix}card-variant-color-rgb: #{to-rgb(color-contrast($color))}; + } +} + +// Box Body +.card-body { + // Tables within the box body + > .table { + margin-bottom: 0; + + > thead > tr > th, + > thead > tr > td { + border-top-width: 0; + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_compact-mode.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_compact-mode.scss new file mode 100644 index 00000000..22cb5ea4 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_compact-mode.scss @@ -0,0 +1,53 @@ +.compact-mode { + .app-header { + max-height: $lte-app-header-height-compact; + + .nav-link { + max-height: $nav-link-height-compact; + } + } + + .nav-link { + --bs-nav-link-padding-y: .25rem; + --bs-nav-link-padding-x: .5rem; + } + + &.sidebar-mini.sidebar-collapse { + .app-sidebar:not(:hover) { + min-width: $lte-sidebar-mini-width-compact; + max-width: $lte-sidebar-mini-width-compact; + + .sidebar-menu { + .nav-link { + width: $lte-sidebar-mini-width-compact - $lte-sidebar-padding-x * 2 !important; + } + } + } + } + + .logo-xs, + .logo-xl { + max-height: $lte-app-header-height-compact; + } + + .brand-image { + width: $nav-link-height-compact; + height: $nav-link-height-compact; + } + + .sidebar-brand { + height: $lte-app-header-height-compact; + } + + .app-footer { + padding: $lte-app-footer-padding-compact; + } + + .sidebar-wrapper { + .nav-icon { + min-width: 1.1rem; + max-width: 1.1rem; + } + } +} + diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_direct-chat.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_direct-chat.scss new file mode 100644 index 00000000..ed961f3e --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_direct-chat.scss @@ -0,0 +1,235 @@ +// +// Component: Direct Chat +// + +.direct-chat { + .card-body { + position: relative; + padding: 0; + overflow-x: hidden; + } + + &.chat-pane-open { + .direct-chat-contacts { + transform: translate(0, 0); + } + } + + + &.timestamp-light { + .direct-chat-timestamp { + color: rgba(var(--#{$prefix}body-color-rgb), .65); + } + } + + &.timestamp-dark { + .direct-chat-timestamp { + color: rgba(var(--#{$prefix}body-color-rgb), .9); + } + } +} + +.direct-chat-messages { + height: 250px; + padding: 10px; + overflow: auto; + transform: translate(0, 0); +} + +.direct-chat-msg, +.direct-chat-text { + display: block; +} + +.direct-chat-msg { + @include clearfix(); + margin-bottom: 10px; +} + +.direct-chat-messages, +.direct-chat-contacts { + @include transition(transform .5s ease-in-out); +} + +.direct-chat-text { + @if $enable-rounded { + @include border-radius($border-radius-lg); + } + + position: relative; + padding: 5px 10px; + margin: 5px 0 0 50px; + color: $lte-direct-chat-default-font-color; + background-color: $lte-direct-chat-default-msg-bg; + border: 1px solid $lte-direct-chat-default-msg-border-color; + + //Create the arrow + &::after, + &::before { + position: absolute; + top: 15px; + right: 100%; + width: 0; + height: 0; + pointer-events: none; + content: " "; + border: solid transparent; + border-right-color: $lte-direct-chat-default-msg-border-color; + } + + &::after { + margin-top: -5px; + border-width: 5px; + } + + &::before { + margin-top: -6px; + border-width: 6px; + } + + .end & { + margin-right: 50px; + margin-left: 0; + + &::after, + &::before { + right: auto; + left: 100%; + border-right-color: transparent; + border-left-color: $lte-direct-chat-default-msg-border-color; + } + } +} + +.direct-chat-img { + @include border-radius(50%); + float: left; + width: 40px; + height: 40px; + + .end & { + float: right; + } +} + +.direct-chat-infos { + display: block; + margin-bottom: 2px; + font-size: $font-size-sm; +} + +.direct-chat-name { + font-weight: 600; +} + +.direct-chat-timestamp { + color: rgba(var(--#{$prefix}body-color-rgb), .75); +} + +//Direct chat contacts pane +.direct-chat-contacts-open { + .direct-chat-contacts { + transform: translate(0, 0); + } +} + +.direct-chat-contacts { + position: absolute; + top: 0; + bottom: 0; + width: 100%; + height: 250px; + overflow: auto; + color: var(--#{$prefix}body-bg); + background-color: var(--#{$prefix}body-color); + transform: translate(101%, 0); +} + +.direct-chat-contacts-light { + background-color: var(--#{$prefix}light-bg-subtle); + + .contacts-list-name { + color: var(--#{$prefix}body-color); + } + + .contacts-list-date { + color: var(--#{$prefix}secondary-color); + } + + .contacts-list-msg { + color: var(--#{$prefix}secondary-color); + } +} + +//Contacts list -- for displaying contacts in direct chat contacts pane +.contacts-list { + @include list-unstyled(); + + > li { + @include clearfix(); + padding: 10px; + margin: 0; + text-decoration: none; + border-bottom: 1px solid rgba($black, .2); + + &:last-of-type { + border-bottom: 0; + } + + a { + text-decoration: none; + } + } +} + +.contacts-list-img { + @include border-radius(50%); + float: left; + width: 40px; +} + +.contacts-list-info { + margin-left: 45px; + color: var(--#{$prefix}body-bg); +} + +.contacts-list-name, +.contacts-list-status { + display: block; +} + +.contacts-list-name { + font-weight: 600; +} + +.contacts-list-status { + font-size: $font-size-sm; +} + +.contacts-list-date { + font-weight: 400; + color: var(--#{$prefix}secondary-bg); +} + +.contacts-list-msg { + color: var(--#{$prefix}secondary-bg); +} + +.end > .direct-chat-text { + color: var(--#{$lte-prefix}direct-chat-color); + background-color: var(--#{$lte-prefix}direct-chat-bg); + border-color: var(--#{$lte-prefix}direct-chat-bg); + + &::after, + &::before { + border-left-color: var(--#{$lte-prefix}direct-chat-bg); + } +} + +// Color variants +@each $name, $color in $theme-colors { + .direct-chat-#{$name} { + --#{$lte-prefix}direct-chat-color: #{color-contrast($color)}; + --#{$lte-prefix}direct-chat-bg: #{$color}; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_docs.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_docs.scss new file mode 100644 index 00000000..4e9bdc10 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_docs.scss @@ -0,0 +1,820 @@ +// +// Component: Docs +// +// Visual treatment for documentation pages. Body class `.docs-page` opts in. +// Demo pages are not affected. +// + +// Inline code blocks rendered by MDX (Astro highlight default class) +.astro-code { + padding: .75rem; + @include border-radius($border-radius); +} + +.docs-page { + // Constrain the docs card to a comfortable reading width. + // Tables and code blocks inside can still scroll horizontally if needed. + .app-content > .container-fluid { + max-width: 60rem; // ~960px + + @include media-breakpoint-up(xxl) { + max-width: 64rem; + } + } + + // + // Card body — breathing room + prose typography. + // Only applied inside docs content cards so demo-page card content + // stays unchanged. + // + .card > .card-body { + padding: 1.5rem; + font-size: 1rem; + line-height: 1.7; + color: var(--#{$prefix}body-color); + + @include media-breakpoint-up(md) { + padding: 2.5rem; + } + + // + // Heading hierarchy. The MDX content in src/html/components/docs/ uses + // ##### (h5) as the dominant section heading and ###### (h6) as a + // sub-heading (most visible in FAQ.mdx, where h5 is a topic and h6 is + // a question). Style h5 as a major section and h6 as a sub-section so + // the visual hierarchy matches the semantic one. + // + h1, + h2, + h3, + h4, + h5, + h6, + .h1, + .h2, + .h3, + .h4, + .h5, + .h6 { + margin-top: 2em; + margin-bottom: .5em; + font-weight: 600; + line-height: 1.3; + color: var(--#{$prefix}emphasis-color); + scroll-margin-top: 4rem; // anchor-link friendly + + &:first-child { + margin-top: 0; + } + } + + h2, + .h2 { + padding-bottom: .35em; + font-size: 1.75rem; + border-bottom: 1px solid var(--#{$prefix}border-color); + } + + h3, + .h3 { + font-size: 1.4rem; + } + + h4, + .h4 { + font-size: 1.2rem; + } + + // h5 is the dominant section heading across docs MDX files. Give it + // a visible separator above so sections are easy to scan. + h5, + .h5 { + padding-top: 1.5em; + margin-top: 2.5em; + font-size: 1.3rem; + border-top: 1px solid var(--#{$prefix}border-color); + + &:first-child { + padding-top: 0; + border-top: 0; + } + } + + // h6 is the sub-section / FAQ question style. Smaller than h5 but + // still distinctly heading-like, with a touch of accent colour to + // separate from body text. + h6, + .h6 { + margin-top: 1.75em; + font-size: 1.08rem; + color: var(--#{$prefix}emphasis-color); + } + + // + // Paragraphs and lists. + // + p, + ul, + ol, + dl, + blockquote, + table { + margin-bottom: 1.1rem; + } + + ul, + ol { + padding-left: 1.5rem; + + li { + margin-bottom: .35rem; + } + + // Tighter nested lists + ul, + ol { + margin-top: .35rem; + margin-bottom: 0; + } + } + + // Lead-style first paragraph (just after a heading) + h2 + p, + h3 + p, + h5 + p { + margin-top: 0; + } + + // + // Inline code — pill style that complements MDX's syntax-highlighted blocks. + // + p code, + li code, + td code, + th code, + h2 code, + h3 code, + h4 code, + h5 code, + h6 code { + padding: .15em .4em; + font-size: .875em; + color: var(--#{$prefix}emphasis-color); + background: var(--#{$prefix}tertiary-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius-sm); + } + + // + // Code blocks. + // + pre.astro-code, + pre { + padding: 1rem 1.25rem; + margin-top: 1.25rem; + margin-bottom: 1.5rem; + overflow-x: auto; + font-size: .875rem; + line-height: 1.6; + @include border-radius($border-radius); + + code { + padding: 0; + background: transparent; + border: 0; + } + } + + // + // Tables — reference-doc styling with hairline borders. + // + table { + width: 100%; + margin-top: 1.25rem; + margin-bottom: 1.5rem; + overflow: hidden; + font-size: .95rem; + border-collapse: collapse; + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius-sm); + + th, + td { + padding: .65rem .9rem; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--#{$prefix}border-color); + } + + th { + font-weight: 600; + color: var(--#{$prefix}emphasis-color); + background: var(--#{$prefix}tertiary-bg); + } + + tr:last-child td { + border-bottom: 0; + } + + code { + white-space: nowrap; + } + } + + // + // Blockquotes used as info callouts. + // + blockquote { + padding: .85rem 1.1rem; + margin-top: 1.25rem; + color: var(--#{$prefix}emphasis-color); + background: var(--#{$prefix}info-bg-subtle); + border-left: 4px solid var(--#{$prefix}info-border-subtle); + @include border-radius($border-radius-sm); + + p:last-child { + margin-bottom: 0; + } + + code { + background: rgba(0, 0, 0, .05); + } + } + + // + // Prose links — stand out more than Bootstrap defaults. + // + a:not(.btn):not(.nav-link) { + color: var(--#{$prefix}primary); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + + &:hover { + text-decoration-thickness: 2px; + } + } + + // + // Horizontal rule — used as additional section separator. + // + hr { + margin: 2.5rem 0; + border: 0; + border-top: 1px solid var(--#{$prefix}border-color); + opacity: 1; + } + + // + // Lead paragraph — the first paragraph in any doc is a summary. + // Visually heavier so it functions as a deck. + // + > p:first-child, + > p:first-of-type { + margin-bottom: 2rem; + font-size: 1.125rem; + line-height: 1.6; + color: var(--#{$prefix}body-color); + } + + // + // FAQ / disclosure accordion — uses native
/. + // Add `.faq-item` to
to opt in. + // + details.faq-item { + margin-bottom: .5rem; + overflow: hidden; + background: var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius); + @include transition(border-color .15s ease, box-shadow .15s ease); + + &:hover { + border-color: var(--#{$prefix}primary-border-subtle); + } + + &[open] { + background: var(--#{$prefix}body-bg); + border-color: var(--#{$prefix}primary-border-subtle); + box-shadow: 0 1px 3px rgba(0, 0, 0, .04); + } + + summary { + position: relative; + padding: .9rem 3rem .9rem 1.1rem; + font-size: 1rem; + font-weight: 600; + color: var(--#{$prefix}emphasis-color); + list-style: none; + cursor: pointer; + user-select: none; + @include transition(color .15s ease); + + // Hide the default disclosure triangle in Webkit/Blink + &::-webkit-details-marker { + display: none; + } + + // Hide the marker in modern Firefox too + &::marker { + content: ""; + } + + &:hover { + color: var(--#{$prefix}primary); + } + + // Right-side chevron, rotates on open + &::after { + position: absolute; + top: 50%; + right: 1.1rem; + display: inline-block; + width: .55rem; + height: .55rem; + margin-top: -.4rem; + content: ""; + border-right: 2px solid currentcolor; + border-bottom: 2px solid currentcolor; + opacity: .55; + transform: rotate(45deg); + @include transition(transform .2s ease); + } + } + + &[open] > summary { + color: var(--#{$prefix}primary); + border-bottom: 1px solid var(--#{$prefix}border-color); + + &::after { + margin-top: -.15rem; + opacity: 1; + transform: rotate(-135deg); + } + } + + // Content (everything after ) gets the inner padding + > *:not(summary) { + padding-right: 1.1rem; + padding-left: 1.1rem; + } + + > *:not(summary):first-of-type { + padding-top: 1rem; + } + + > *:not(summary):last-child { + padding-bottom: 1rem; + } + + // Tighten paragraphs/lists inside an FAQ answer + p, + ul, + ol { + margin-bottom: .65rem; + } + + > *:last-child p:last-child, + > p:last-child, + > ul:last-child, + > ol:last-child { + margin-bottom: 0; + } + + // Code blocks inside an FAQ answer get a less-distracting bg + pre.astro-code, + pre { + margin-right: 0; + margin-left: 0; + } + } + } + + // Dark-mode tweak: blockquote info bg needs more contrast on dark + &[data-bs-theme="dark"], + [data-bs-theme="dark"] & { + .card > .card-body blockquote code { + background: rgba(255, 255, 255, .08); + } + } +} + +// +// FAQ page — custom layout that breaks out of the standard docs card +// to give the FAQ a more distinctive visual identity. Opt in via +// body.faq-page (set in pages/docs/faq.astro). +// + +.faq-page { + // The FAQ doesn't use the standard card wrapper used by other docs, + // so the container can stretch wider for the chip strip and section + // grid. + .app-content > .container-fluid { + max-width: 64rem; + + @include media-breakpoint-up(xxl) { + max-width: 72rem; + } + } + + // --- Hero ------------------------------------------------------------- + .faq-hero { + position: relative; + padding: 3rem 1.5rem 2.5rem; + margin-top: 1rem; + overflow: hidden; + background: radial-gradient(ellipse at top left, rgba(var(--#{$prefix}primary-rgb), .12), transparent 60%), radial-gradient(ellipse at bottom right, rgba(var(--#{$prefix}info-rgb), .1), transparent 65%), var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius-xl); + } + + .faq-hero-eyebrow { + display: inline-flex; + gap: .4rem; + align-items: center; + padding: .35rem .85rem; + margin-bottom: 1rem; + font-size: .75rem; + font-weight: 600; + color: var(--#{$prefix}primary); + text-transform: uppercase; + letter-spacing: .08em; + background: var(--#{$prefix}primary-bg-subtle); + border: 1px solid var(--#{$prefix}primary-border-subtle); + @include border-radius(50rem); + + .bi { + font-size: .9rem; + } + } + + .faq-hero-title { + margin: 0 0 .5rem; + font-size: 2.25rem; + font-weight: 700; + line-height: 1.15; + color: var(--#{$prefix}emphasis-color); + + @include media-breakpoint-up(md) { + font-size: 2.75rem; + } + } + + .faq-hero-lead { + max-width: 36rem; + margin: 0 auto 1.75rem; + font-size: 1.05rem; + line-height: 1.55; + color: var(--#{$prefix}secondary-color); + } + + .faq-search { + position: relative; + max-width: 32rem; + margin: 0 auto; + + .form-control { + height: 3rem; + padding-right: 3rem; + padding-left: 3rem; + font-size: 1rem; + background: var(--#{$prefix}body-bg); + @include border-radius(50rem); + @include transition(box-shadow .15s ease, border-color .15s ease); + + &:focus { + box-shadow: 0 0 0 .25rem rgba(var(--#{$prefix}primary-rgb), .15); + } + } + } + + .faq-search-icon { + position: absolute; + top: 50%; + left: 1.2rem; + color: var(--#{$prefix}secondary-color); + pointer-events: none; + transform: translateY(-50%); + } + + .faq-search-clear { + position: absolute; + top: 50%; + right: .65rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + color: var(--#{$prefix}secondary-color); + background: var(--#{$prefix}tertiary-bg); + border: 0; + transform: translateY(-50%); + @include border-radius(50%); + @include transition(background .15s ease, color .15s ease); + + &:hover { + color: var(--#{$prefix}body-color); + background: var(--#{$prefix}secondary-bg); + } + } + + .faq-empty-state { + max-width: 32rem; + padding: 1rem; + margin: 1.5rem auto 0; + font-size: .95rem; + color: var(--#{$prefix}secondary-color); + background: var(--#{$prefix}tertiary-bg); + @include border-radius($border-radius); + + .bi { + margin-right: .35rem; + } + } + + // --- Section nav chips ------------------------------------------------ + .faq-chips { + display: flex; + flex-wrap: wrap; + gap: .5rem; + justify-content: center; + } + + .faq-chip { + display: inline-flex; + gap: .45rem; + align-items: center; + padding: .45rem .85rem; + font-size: .875rem; + font-weight: 500; + color: var(--#{$prefix}body-color); + text-decoration: none; + background: var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius(50rem); + @include transition(transform .15s ease, border-color .15s ease, background-color .15s ease, color .15s ease); + + &:hover { + color: var(--#{$prefix}emphasis-color); + background: var(--#{$prefix}tertiary-bg); + transform: translateY(-1px); + } + + .bi { + font-size: 1rem; + } + } + + .faq-chip-count { + padding: 0 .45rem; + font-size: .7rem; + font-weight: 600; + color: var(--#{$prefix}secondary-color); + background: var(--#{$prefix}tertiary-bg); + @include border-radius(50rem); + } + + // Per-section chip accent on hover/active + @each $tone in (primary, info, warning, success, danger, secondary) { + .faq-chip-#{$tone}:hover { + color: var(--#{$prefix}#{$tone}-text-emphasis); + background: var(--#{$prefix}#{$tone}-bg-subtle); + border-color: var(--#{$prefix}#{$tone}-border-subtle); + + .faq-chip-count { + color: var(--#{$prefix}#{$tone}-text-emphasis); + background: rgba(0, 0, 0, .06); + } + } + } + + // --- Sections --------------------------------------------------------- + .faq-section { + margin-bottom: 2.5rem; + scroll-margin-top: 5rem; + } + + .faq-section-header { + display: flex; + gap: 1rem; + align-items: center; + padding: 1rem 1.25rem; + margin-bottom: 1rem; + background: var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + border-left: 4px solid var(--#{$prefix}primary); + @include border-radius($border-radius); + } + + .faq-section-icon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + font-size: 1.35rem; + color: var(--#{$prefix}primary); + background: var(--#{$prefix}primary-bg-subtle); + @include border-radius($border-radius); + } + + .faq-section-title { + margin: 0; + font-size: 1.4rem; + font-weight: 600; + line-height: 1.2; + color: var(--#{$prefix}emphasis-color); + } + + .faq-section-count { + margin: .1rem 0 0; + font-size: .8rem; + color: var(--#{$prefix}secondary-color); + } + + // Per-section tone accents + @each $tone in (primary, info, warning, success, danger, secondary) { + .faq-section-#{$tone} { + border-left-color: var(--#{$prefix}#{$tone}); + + .faq-section-icon { + color: var(--#{$prefix}#{$tone}-text-emphasis); + background: var(--#{$prefix}#{$tone}-bg-subtle); + } + } + } + + // --- FAQ items (override the generic .docs-page details styling) ------ + .faq-section-items .faq-item { + margin-bottom: .5rem; + overflow: hidden; + background: var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius); + @include transition(border-color .15s ease, box-shadow .15s ease, transform .15s ease); + + &:hover { + border-color: var(--#{$prefix}primary-border-subtle); + transform: translateX(2px); + } + + &[open] { + border-color: var(--#{$prefix}primary-border-subtle); + box-shadow: 0 2px 8px rgba(0, 0, 0, .04); + transform: none; + } + + summary { + display: flex; + gap: .85rem; + align-items: center; + padding: 1rem 1.25rem; + font-size: 1rem; + font-weight: 500; + color: var(--#{$prefix}emphasis-color); + list-style: none; + cursor: pointer; + user-select: none; + + &::-webkit-details-marker { + display: none; + } + + &::marker { + content: ""; + } + + &::after { + content: none; // disable the generic chevron, we use our own + } + } + + .faq-q-icon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + font-size: .85rem; + color: var(--#{$prefix}primary); + background: var(--#{$prefix}primary-bg-subtle); + @include border-radius(50%); + } + + .faq-q-text { + flex-grow: 1; + } + + .faq-q-chevron { + flex-shrink: 0; + color: var(--#{$prefix}secondary-color); + @include transition(transform .2s ease, color .2s ease); + } + + &[open] .faq-q-chevron { + color: var(--#{$prefix}primary); + transform: rotate(180deg); + } + + &[open] summary { + color: var(--#{$prefix}primary); + border-bottom: 1px solid var(--#{$prefix}border-color); + } + + .faq-answer { + padding: 1.1rem 1.25rem 1.1rem 3.85rem; // align with the question text (icon + gap) + font-size: .95rem; + line-height: 1.65; + color: var(--#{$prefix}body-color); + + p:last-child, + ul:last-child, + ol:last-child, + pre:last-child { + margin-bottom: 0; + } + + a { + color: var(--#{$prefix}primary); + text-decoration: underline; + text-underline-offset: 2px; + + &:hover { + text-decoration-thickness: 2px; + } + } + + code { + padding: .12em .35em; + font-size: .875em; + color: var(--#{$prefix}emphasis-color); + background: var(--#{$prefix}tertiary-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius-sm); + } + + pre.astro-code, + pre { + padding: .85rem 1rem; + margin-top: .75rem; + margin-bottom: .75rem; + overflow-x: auto; + font-size: .85rem; + line-height: 1.55; + @include border-radius($border-radius-sm); + + code { + padding: 0; + background: transparent; + border: 0; + } + } + } + } + + // --- CTA footer ------------------------------------------------------- + .faq-cta { + padding: 2.5rem 1.5rem; + text-align: center; + background: radial-gradient(ellipse at top, rgba(var(--#{$prefix}primary-rgb), .08), transparent 60%), var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + @include border-radius($border-radius-xl); + } + + .faq-cta-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 3.5rem; + height: 3.5rem; + margin-bottom: 1rem; + font-size: 1.6rem; + color: var(--#{$prefix}primary); + background: var(--#{$prefix}primary-bg-subtle); + @include border-radius(50%); + } + + .faq-cta h2 { + margin: 0 0 .5rem; + font-size: 1.5rem; + font-weight: 600; + color: var(--#{$prefix}emphasis-color); + } + + .faq-cta p { + max-width: 32rem; + margin: 0 auto 1.5rem; + color: var(--#{$prefix}secondary-color); + } + + .faq-cta-actions { + display: inline-flex; + flex-wrap: wrap; + gap: .5rem; + justify-content: center; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_dropdown.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_dropdown.scss new file mode 100644 index 00000000..9ddf2f7c --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_dropdown.scss @@ -0,0 +1,225 @@ +// +// Component: Dropdown +// + +// General Dropdown Rules + +// Ensure children cannot overflow and break the dropdown border radius +.dropdown-menu { + overflow: hidden; +} + +.fs-7 { + .dropdown-menu { + font-size: $font-size-sm !important; + } + + .dropdown-toggle::after { + vertical-align: .2rem; + } +} + +.dropdown-item-title { + margin: 0; + font-size: $font-size-base; +} + +.dropdown-icon { + &::after { + margin-left: 0; + } +} + +// Dropdown Sizes +.dropdown-menu-lg { + min-width: 280px; + max-width: 300px; + padding: 0; + + .dropdown-divider { + margin: 0; + } + + .dropdown-item { + padding: $dropdown-padding-y $dropdown-item-padding-x; + } + + p { + margin: 0; + word-wrap: break-word; + white-space: normal; + } +} + +// Dropdown Submenu +.dropdown-submenu { + position: relative; + + > a::after { + @include caret-end(); + float: right; + margin-top: .5rem; + margin-left: .5rem; + } + + > .dropdown-menu { + top: 0; + left: 100%; + margin-top: 0; + margin-left: 0; + } +} + +// Dropdown Hover +.dropdown-hover { + &:hover, + &.nav-item.dropdown:hover, + .dropdown-submenu:hover, + &.dropdown-submenu:hover { + > .dropdown-menu { + display: block; + } + } +} + + +// Dropdown Sizes +.dropdown-menu-xl { + min-width: 360px; + max-width: 420px; + padding: 0; + + .dropdown-divider { + margin: 0; + } + + .dropdown-item { + padding: $dropdown-padding-y $dropdown-item-padding-x; + } + + p { + margin: 0; + word-wrap: break-word; + white-space: normal; + } +} + +// Dropdown header and footer +.dropdown-footer, +.dropdown-header { + display: block; + padding: .5rem $dropdown-item-padding-x; + font-size: $font-size-sm; + text-align: center; +} + +// Add fade animation to dropdown menus by appending +// the class .animated-dropdown-menu to the .dropdown-menu ul (or ol) +.open:not(.dropup) > .animated-dropdown-menu { + animation: flipInX .7s both; + backface-visibility: visible !important; +} + +// Fix dropdown menu in navbars +.navbar-custom-menu > .navbar-nav { + > li { + position: relative; + > .dropdown-menu { + position: absolute; + right: 0; + left: auto; + } + } +} + +@include media-breakpoint-down(sm) { + .navbar-custom-menu > .navbar-nav { + float: right; + > li { + position: static; + > .dropdown-menu { + position: absolute; + right: 5%; + left: auto; + background-color: var(--#{$prefix}body-bg); + border: 1px solid var(--#{$prefix}border-color); + } + } + } +} + +// User Menu +.navbar-nav > .user-menu { + > .nav-link::after { + content: none; + } + + > .dropdown-menu { + width: 280px; + padding: 0; + + // Header menu + > li.user-header { + min-height: 175px; + padding: 10px; + text-align: center; + + // User image + > img { + z-index: 5; + width: 90px; + height: 90px; + border: 3px solid; + border-color: transparent; + border-color: var(--#{$prefix}border-color-translucent); + } + + > p { + z-index: 5; + margin-top: 10px; + font-size: 17px; + word-wrap: break-word; + + > small { + display: block; + font-size: 12px; + } + } + } + + // Menu Body + > .user-body { + @include clearfix(); + padding: 15px; + border-top: 1px solid var(--#{$prefix}border-color); + border-bottom: 1px solid var(--#{$prefix}border-color-translucent); + + a { + text-decoration: none; + } + } + + // Menu Footer + > .user-footer { + @include clearfix(); + padding: 10px; + background-color: var(--#{$prefix}light-bg); + } + } + + .user-image { + @include media-breakpoint-up(sm) { + float: none; + margin-top: -8px; + margin-right: .4rem; + line-height: 10px; + } + + float: left; + width: $lte-sidebar-user-image-width; + height: $lte-sidebar-user-image-width; + margin-top: -2px; + // margin-right: 10px; + @include border-radius(50%); + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_info-box.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_info-box.scss new file mode 100644 index 00000000..ad65d71e --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_info-box.scss @@ -0,0 +1,131 @@ +// +// Component: Info Box +// + +.info-box { + @include box-shadow($lte-card-shadow); + @include border-radius($border-radius); + + position: relative; + display: flex; + width: 100%; + min-height: 80px; + padding: .5rem; + margin-bottom: map-get($spacers, 3); + color: var(--#{$prefix}body-color); + background-color: var(--#{$prefix}body-bg); + + .progress { + height: 2px; + margin: 5px 0; + background-color: rgba(var(--#{$lte-prefix}card-variant-color-rgb), .125); + + .progress-bar { + background-color: var(--#{$lte-prefix}card-variant-color); + } + } + + .info-box-icon { + display: flex; + align-items: center; + justify-content: center; + width: 70px; + font-size: 1.875rem; + text-align: center; + @include border-radius($border-radius); + + > img { + max-width: 100%; + } + } + + .info-box-content { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + padding: 0 10px; + line-height: 1.8; + } + + .info-box-number { + display: block; + margin-top: .25rem; + font-weight: $font-weight-bold; + } + + .progress-description, + .info-box-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .info-box-more { + display: block; + } + + .progress-description { + margin: 0; + + } + + @include media-breakpoint-up(md) { + .col-xl-2 &, + .col-lg-2 &, + .col-md-2 & { + .progress-description { + display: none; + } + } + + .col-xl-3 &, + .col-lg-3 &, + .col-md-3 & { + .progress-description { + display: none; + } + } + } + + @include media-breakpoint-up(lg) { + .col-xl-2 &, + .col-lg-2 &, + .col-md-2 & { + .progress-description { + @include font-size(.75rem); + display: block; + } + } + + .col-xl-3 &, + .col-lg-3 &, + .col-md-3 & { + .progress-description { + @include font-size(.75rem); + display: block; + } + } + } + + @include media-breakpoint-up(xl) { + .col-xl-2 &, + .col-lg-2 &, + .col-md-2 & { + .progress-description { + @include font-size(1rem); + display: block; + } + } + + .col-xl-3 &, + .col-lg-3 &, + .col-md-3 & { + .progress-description { + @include font-size(1rem); + display: block; + } + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_miscellaneous.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_miscellaneous.scss new file mode 100644 index 00000000..e04ec287 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_miscellaneous.scss @@ -0,0 +1,22 @@ +// +// Misc: Miscellaneous +// + +// Image sizes +.img-size-64, +.img-size-50, +.img-size-32 { + height: auto; +} + +.img-size-64 { + width: 64px; +} + +.img-size-50 { + width: 50px; +} + +.img-size-32 { + width: 32px; +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_mixins.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_mixins.scss new file mode 100644 index 00000000..44f6c188 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_mixins.scss @@ -0,0 +1,6 @@ +// +// General: Mixins +// + +@import "mixins/animations"; +@import "mixins/scrollbar"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_progress-bars.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_progress-bars.scss new file mode 100644 index 00000000..c99605da --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_progress-bars.scss @@ -0,0 +1,66 @@ +// +// Component: Progress Bar +// + +//General CSS +.progress { + @include box-shadow(null); + @include border-radius($lte-progress-bar-border-radius); + + // Vertical bars + &.vertical { + position: relative; + display: inline-block; + width: 30px; + height: 200px; + margin-right: 10px; + + > .progress-bar { + position: absolute; + bottom: 0; + width: 100%; + } + + //Sizes + &.sm, + &.progress-sm { + width: 20px; + } + + &.xs, + &.progress-xs { + width: 10px; + } + + &.xxs, + &.progress-xxs { + width: 3px; + } + } +} + +.progress-group { + margin-bottom: map-get($spacers, 2); +} + +// size variation +.progress-sm { + height: 10px; +} + +.progress-xs { + height: 7px; +} + +.progress-xxs { + height: 3px; +} + +// Remove margins from progress bars when put in a table +.table { + tr > td { + .progress { + margin: 0; + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_root.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_root.scss new file mode 100644 index 00000000..2633fbd2 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_root.scss @@ -0,0 +1,5 @@ +:root, +[data-bs-theme="light"] { + // Sidebar + --#{$lte-prefix}sidebar-width: #{$lte-sidebar-width}; +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_small-box.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_small-box.scss new file mode 100644 index 00000000..ce2e4863 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_small-box.scss @@ -0,0 +1,127 @@ +// +// Component: Small Box +// + +.small-box { + @include border-radius($border-radius); + @include box-shadow($lte-card-shadow); + position: relative; + display: block; + margin-bottom: 1.25rem; + --bs-link-color-rgb: none; + --bs-link-hover-color-rgb: none; + --bs-heading-color: none; + + // content wrapper + > .inner { + padding: 10px; + } + + > .small-box-footer { + position: relative; + z-index: 10; + display: block; + padding: 3px 0; + text-align: center; + background-color: rgba($black, .07); + + &:hover { + background-color: rgba($black, .1); + } + } + + h3 { + @include font-size(2.2rem); + padding: 0; + margin: 0 0 10px; + font-weight: 700; + white-space: nowrap; + } + + @include media-breakpoint-up(lg) { + .col-xl-2 &, + .col-lg-2 &, + .col-md-2 & { + h3 { + @include font-size(1.6rem); + } + } + + .col-xl-3 &, + .col-lg-3 &, + .col-md-3 & { + h3 { + @include font-size(1.6rem); + } + } + } + + @include media-breakpoint-up(xl) { + .col-xl-2 &, + .col-lg-2 &, + .col-md-2 & { + h3 { + @include font-size(2.2rem); + } + } + + .col-xl-3 &, + .col-lg-3 &, + .col-md-3 & { + h3 { + @include font-size(2.2rem); + } + } + } + + p { + font-size: 1rem; + + > small { + display: block; + margin-top: 5px; + font-size: .9rem; + color: $gray-100; + } + } + + h3, + p { + z-index: 5; + } + + // the icon + .small-box-icon { + position: absolute; + top: 15px; + right: 15px; + z-index: 0; + height: 70px; + font-size: 70px; + color: rgba($black, .15); + @include transition(transform $lte-transition-speed linear); + } + + // Small box hover state + &:hover { + // Animate icons on small box hover + .small-box-icon { + transform: scale(1.1); + } + } +} + +@include media-breakpoint-down(sm) { + // No need for icons on very small devices + .small-box { + text-align: center; + + .small-box-icon { + display: none; + } + + p { + font-size: 12px; + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_table.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_table.scss new file mode 100644 index 00000000..2bac75b7 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_table.scss @@ -0,0 +1,67 @@ +// +// Component: Table +// + +.table { + &:not(.table-dark) { + color: inherit; + } + + // fixed table head + // Uses Bootstrap CSS variables so the sticky header follows light/dark mode + // automatically. Fixes #6026. + &.table-head-fixed { + thead tr:nth-child(1) th { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--bs-body-bg, #{$white}); + border-bottom: 0; + box-shadow: inset 0 1px 0 var(--bs-border-color, #{$table-border-color}), inset 0 -1px 0 var(--bs-border-color, #{$table-border-color}); + } + } + + // no border + &.no-border { + &, + td, + th { + border: 0; + } + } + + // .text-center in tables + &.text-center { + &, + td, + th { + text-align: center; + } + } + + &.table-valign-middle { + thead > tr > th, + thead > tr > td, + tbody > tr > th, + tbody > tr > td { + vertical-align: middle; + } + } + + .card-body.p-0 & { + thead > tr > th, + thead > tr > td, + tfoot > tr > th, + tfoot > tr > td, + tbody > tr > th, + tbody > tr > td { + &:first-of-type { + padding-left: map-get($spacers, 4); + } + + &:last-of-type { + padding-right: map-get($spacers, 4); + } + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_timeline.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_timeline.scss new file mode 100644 index 00000000..f1521aee --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_timeline.scss @@ -0,0 +1,121 @@ +// +// Component: Timeline +// + +.timeline { + position: relative; + padding: 0; + margin: 0 0 45px; + // The line + &::before { + @include border-radius($border-radius); + position: absolute; + top: 0; + bottom: 0; + left: 31px; + width: 4px; + margin: 0; + content: ""; + background-color: var(--#{$prefix}border-color); + } + // Element + > div { + &::before, + &::after { + display: table; + content: ""; + } + + position: relative; + margin-right: 10px; + margin-bottom: 15px; + // The content + > .timeline-item { + @include box-shadow($lte-card-shadow); + @include border-radius($border-radius); + position: relative; + padding: 0; + margin-top: 0; + margin-right: 15px; + margin-left: 60px; + color: var(--#{$prefix}body-color); + background-color: var(--#{$prefix}body-bg); + // The time and header + > .time { + float: right; + padding: 10px; + font-size: 12px; + color: var(--#{$prefix}secondary-color); + } + // Header + > .timeline-header { + padding: 10px; + margin: 0; + font-size: 16px; + line-height: 1.1; + color: var(--#{$prefix}secondary-color); + border-bottom: 1px solid var(--#{$prefix}border-color); + // Link in header + > a { + font-weight: 600; + text-decoration: none; + } + } + // Item body and footer + > .timeline-body, + > .timeline-footer { + padding: 10px; + } + + > .timeline-body { + > img { + margin: 10px; + } + > dl, + ol, + ul { + margin: 0; + } + } + + + } + + .timeline-icon { + position: absolute; + top: 0; + left: 18px; + width: 30px; + height: 30px; + font-size: 16px; + line-height: 30px; + text-align: center; + background-color: var(--#{$prefix}secondary-bg); + border-radius: 50%; // stylelint-disable-line property-disallowed-list + } + } + // Time label + > .time-label { + > span { + @include border-radius(4px); + display: inline-block; + padding: 5px; + font-weight: 600; + background-color: var(--#{$prefix}body-bg); + } + } +} + +.timeline-inverse { + > div { + > .timeline-item { + @include box-shadow(none); + background-color: var(--#{$prefix}tertiary-bg); + border: 1px solid var(--#{$prefix}border-color); + + > .timeline-header { + border-bottom-color: var(--#{$prefix}border-color); + } + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_toasts.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_toasts.scss new file mode 100644 index 00000000..14be1ce6 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_toasts.scss @@ -0,0 +1,33 @@ +// +// Toast +// + +@each $name, $color in $theme-colors { + .toast-#{$name} { + --#{$prefix}toast-header-color: #{color-contrast($color)}; + --#{$prefix}toast-header-bg: #{$color}; + --#{$prefix}toast-header-border-color: #{$color}; + --#{$prefix}toast-border-color: #{$color}; + --#{$prefix}toast-bg: var(--#{$prefix}#{$name}-bg-subtle); + + @if color-contrast($color) == $color-contrast-light { + .btn-close { + @include btn-close-white(); + } + } + } +} + +@if $enable-dark-mode { + @include color-mode(dark) { + @each $name, $color in $theme-colors { + .toast-#{$name} { + @if color-contrast($color) == $color-contrast-dark { + .btn-close { + --#{$prefix}btn-close-white-filter: none; + } + } + } + } + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables-dark.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables-dark.scss new file mode 100644 index 00000000..9bd8681f --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables-dark.scss @@ -0,0 +1,16 @@ +// SIDEBAR SKINS +// -------------------------------------------------------- + +$lte-sidebar-hover-bg-dark: rgba(255, 255, 255, .1) !default; +$lte-sidebar-color-dark: #c2c7d0 !default; +$lte-sidebar-hover-color-dark: $white !default; +$lte-sidebar-active-color-dark: $white !default; +$lte-sidebar-menu-active-bg-dark: rgba(255, 255, 255, .1) !default; +$lte-sidebar-menu-active-color-dark: $white !default; +$lte-sidebar-submenu-bg-dark: transparent !default; +$lte-sidebar-submenu-color-dark: #c2c7d0 !default; +$lte-sidebar-submenu-hover-color-dark: $white !default; +$lte-sidebar-submenu-hover-bg-dark: rgba(255, 255, 255, .1) !default; +$lte-sidebar-submenu-active-color-dark: $white !default; +$lte-sidebar-submenu-active-bg-dark: rgba(255, 255, 255, .1) !default; +$lte-sidebar-header-color-dark: tint-color(#c2c7d0, 5%) !default; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables.scss new file mode 100644 index 00000000..35675327 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/_variables.scss @@ -0,0 +1,131 @@ +// +// Custom AdminLTE Variables +// + +// Prefix for :root CSS variables and others. +$lte-prefix: lte- !default; + +// TRANSITIONS SETTINGS +// -------------------------------------------------------- +// Transition global options +$lte-transition-speed: .3s !default; +$lte-transition-fn: ease-in-out !default; + +// SIDEBAR +// -------------------------------------------------------- +$lte-sidebar-width: 250px !default; +$lte-sidebar-breakpoint: lg !default; +$lte-sidebar-padding-x: .5rem !default; +$lte-sidebar-padding-x-compact: .5rem !default; +$lte-sidebar-padding-y: .5rem !default; +$lte-sidebar-padding-y-compact: .25rem !default; +$lte-sidebar-transition: min-width $lte-transition-speed $lte-transition-fn, + max-width $lte-transition-speed $lte-transition-fn, + margin-left $lte-transition-speed $lte-transition-fn, + margin-right $lte-transition-speed $lte-transition-fn !default; + +// SIDEBAR SKINS +// -------------------------------------------------------- + +$lte-sidebar-hover-bg: rgba($black, .1) !default; +$lte-sidebar-color: $gray-800 !default; +$lte-sidebar-hover-color: $gray-900 !default; +$lte-sidebar-active-color: $black !default; +$lte-sidebar-menu-active-bg: rgba($black, .1) !default; +$lte-sidebar-menu-active-color: $black !default; +$lte-sidebar-submenu-bg: transparent !default; +$lte-sidebar-submenu-color: #777 !default; +$lte-sidebar-submenu-hover-color: $black !default; +$lte-sidebar-submenu-hover-bg: rgba($black, .1) !default; +$lte-sidebar-submenu-active-color: $gray-900 !default; +$lte-sidebar-submenu-active-bg: rgba($black, .1) !default; +$lte-sidebar-header-color: shade-color($gray-800, 5%) !default; + +// SIDEBAR MINI +// -------------------------------------------------------- +$nav-link-padding-x-compact: .25rem !default; +$lte-sidebar-mini-width: ($nav-link-padding-x + $lte-sidebar-padding-x + .8rem) * 2 !default; +$lte-sidebar-mini-width-compact: ($nav-link-padding-x-compact + $lte-sidebar-padding-x-compact + .8rem) * 2 !default; +$lte-sidebar-nav-icon-width: $lte-sidebar-mini-width - (($lte-sidebar-padding-x + $nav-link-padding-x) * 2) !default; +$lte-sidebar-user-image-width: $lte-sidebar-nav-icon-width + ($nav-link-padding-x * .4) !default; + +// MAIN HEADER +// -------------------------------------------------------- +$nav-link-height-compact: 1.75rem !default; +$lte-app-header-bottom-border-width: $border-width !default; +$lte-app-header-bottom-border-color: var(--#{$prefix}border-color) !default; +$lte-app-header-bottom-border: $lte-app-header-bottom-border-width solid $lte-app-header-bottom-border-color !default; +$lte-app-header-link-padding-y: $navbar-padding-y !default; +$lte-app-header-height: ($nav-link-height + ($lte-app-header-link-padding-y * 2)) !default; +$lte-app-header-height-compact: ($nav-link-height-compact + ($lte-app-header-link-padding-y * 2)) !default; +$lte-zindex-fixed-header: $zindex-fixed !default; + +// APP MAIN +// -------------------------------------------------------- +$lte-app-main-padding-bottom: $grid-gutter-width * .5 !default; + +// CONTENT PADDING +// -------------------------------------------------------- +$lte-content-padding-y: 0 !default; +$lte-content-padding-x: .5rem !default; + +// MAIN FOOTER +// -------------------------------------------------------- +$lte-app-footer-padding: 1rem !default; +$lte-app-footer-padding-compact: .5rem !default; +$lte-app-footer-border-top-width: 1px !default; +$lte-app-footer-border-top-color: var(--#{$prefix}border-color) !default; +$lte-app-footer-border-top: $lte-app-footer-border-top-width solid $lte-app-footer-border-top-color !default; +$lte-app-footer-bg: var(--#{$prefix}body-bg) !default; +$lte-app-footer-color: var(--#{$prefix}secondary-color) !default; +$lte-zindex-fixed-footer: $zindex-fixed !default; +// CONTENT BOTTOM AREA +// -------------------------------------------------------- +$lte-app-content-bottom-area-margin-bottom: -$lte-app-main-padding-bottom !default; +$lte-app-content-bottom-area-color: $lte-app-footer-color !default; +$lte-app-content-bottom-area-bg: $lte-app-footer-bg !default; +$lte-app-content-bottom-area-top-border: $lte-app-footer-border-top !default; +$lte-app-content-bottom-area-padding-y: $lte-app-footer-padding !default; +$lte-app-content-bottom-area-padding-x: 0 !default; + +// CONTENT TOP AREA +// -------------------------------------------------------- +$lte-app-content-top-area-top-border: $lte-app-footer-border-top !default; +$lte-app-content-top-area-padding-y: $lte-app-footer-padding !default; +$lte-app-content-top-area-padding-x: 0 !default; + +// BRAND LINK +// -------------------------------------------------------- +$navbar-brand-padding-y-compact: $navbar-brand-padding-y * .75 !default; +$navbar-padding-y-compact: $navbar-padding-y * .5 !default; +$lte-brand-link-padding-y: $navbar-brand-padding-y + $navbar-padding-y !default; +$lte-brand-link-padding-y-compact: $navbar-brand-padding-y-compact + $navbar-padding-y-compact !default; +$lte-brand-link-padding-x: $lte-sidebar-padding-x !default; +$lte-brand-link-padding-x-compact: $lte-sidebar-padding-x-compact !default; +$lte-brand-link-border-buttom: 1px !default; + +// CARDS +// -------------------------------------------------------- +$lte-card-shadow: 0 0 1px rgba(var(--#{$prefix}body-color-rgb), .125), 0 1px 3px rgba(var(--#{$prefix}body-color-rgb), .2) !default; +$lte-card-title-font-size: 1.1rem !default; +$lte-card-title-font-weight: $font-weight-normal !default; + +// PROGRESS BARS +// -------------------------------------------------------- +$lte-progress-bar-border-radius: 1px !default; + +// CALLOUTS +// -------------------------------------------------------- +$lte-callout-link-font-weight: $alert-link-font-weight !default; + +// DIRECT CHAT +// -------------------------------------------------------- +$lte-direct-chat-default-msg-bg: var(--#{$prefix}secondary-bg) !default; +$lte-direct-chat-default-font-color: var(--#{$prefix}emphasis-color) !default; +$lte-direct-chat-default-msg-border-color: var(--#{$prefix}border-color) !default; + +// Z-INDEX +// -------------------------------------------------------- +$lte-zindex-app-header: $zindex-fixed + 4 !default; +$lte-zindex-sidebar: $zindex-fixed + 8 !default; +$lte-zindex-sidebar-overlay: $lte-zindex-sidebar - 1 !default; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/adminlte.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/adminlte.scss new file mode 100644 index 00000000..47a13250 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/adminlte.scss @@ -0,0 +1,81 @@ +/*! + * AdminLTE v4.0.0 + * Author: Colorlib + * Website: AdminLTE.io + * License: Open source - MIT + */ + +// Bootstrap Configuration +// --------------------------------------------------- +@import "bootstrap/scss/functions"; + +// AdminLTE Configuration +// --------------------------------------------------- +@import "bootstrap-variables"; // little modified are here + +// Bootstrap Configuration +// --------------------------------------------------- +@import "bootstrap/scss/variables"; +@import "bootstrap/scss/variables-dark"; +@import "bootstrap/scss/maps"; +@import "bootstrap/scss/mixins"; +@import "bootstrap/scss/utilities"; + +// Bootstrap Layout & components +@import "bootstrap/scss/root"; +@import "bootstrap/scss/reboot"; +@import "bootstrap/scss/type"; +@import "bootstrap/scss/images"; +@import "bootstrap/scss/containers"; +@import "bootstrap/scss/grid"; +@import "bootstrap/scss/tables"; +@import "bootstrap/scss/forms"; +@import "bootstrap/scss/buttons"; +@import "bootstrap/scss/transitions"; +@import "bootstrap/scss/dropdown"; +@import "bootstrap/scss/button-group"; +@import "bootstrap/scss/nav"; +@import "bootstrap/scss/navbar"; +@import "bootstrap/scss/card"; +@import "bootstrap/scss/accordion"; +@import "bootstrap/scss/breadcrumb"; +@import "bootstrap/scss/pagination"; +@import "bootstrap/scss/badge"; +@import "bootstrap/scss/alert"; +@import "bootstrap/scss/progress"; +@import "bootstrap/scss/list-group"; +@import "bootstrap/scss/close"; +@import "bootstrap/scss/toasts"; +@import "bootstrap/scss/modal"; +@import "bootstrap/scss/tooltip"; +@import "bootstrap/scss/popover"; +@import "bootstrap/scss/carousel"; +@import "bootstrap/scss/spinners"; +@import "bootstrap/scss/offcanvas"; +@import "bootstrap/scss/placeholders"; + +// Bootstrap Helpers +@import "bootstrap/scss/helpers"; + +// Bootstrap Utilities +@import "bootstrap/scss/utilities/api"; + +// AdminLTE Configuration +// --------------------------------------------------- +@import "variables"; +@import "variables-dark"; +@import "mixins"; + +// AdiminLTE Parts +// --------------------------------------------------- +@import "parts/core"; +@import "parts/components"; +@import "parts/extra-components"; +@import "parts/pages"; +@import "parts/miscellaneous"; + +// AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance +@import "accessibility"; + +// AdminLTE Documentation Styles (only applies to pages with body.docs-page) +@import "docs"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_animations.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_animations.scss new file mode 100644 index 00000000..5a403175 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_animations.scss @@ -0,0 +1,116 @@ +// +// Mixins: Animation +// + + +@keyframes flipInX { + 0% { + opacity: 0; + transition-timing-function: ease-in; + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + } + + 40% { + transition-timing-function: ease-in; + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + } + + 60% { + opacity: 1; + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + } + + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + 100% { + transform: perspective(400px); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes shake { + 0% { + transform: translate(2px, 1px) rotate(0deg); + } + 10% { + transform: translate(-1px, -2px) rotate(-2deg); + } + 20% { + transform: translate(-3px, 0) rotate(3deg); + } + 30% { + transform: translate(0, 2px) rotate(0deg); + } + 40% { + transform: translate(1px, -1px) rotate(1deg); + } + 50% { + transform: translate(-1px, 2px) rotate(-1deg); + } + 60% { + transform: translate(-3px, 1px) rotate(0deg); + } + 70% { + transform: translate(2px, 1px) rotate(-2deg); + } + 80% { + transform: translate(-1px, -1px) rotate(4deg); + } + 90% { + transform: translate(2px, 2px) rotate(0deg); + } + 100% { + transform: translate(1px, -2px) rotate(-1deg); + } +} + +@keyframes wobble { + 0% { + transform: none; + } + + 15% { + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + 100% { + transform: none; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_scrollbar.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_scrollbar.scss new file mode 100644 index 00000000..65c3b53d --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/mixins/_scrollbar.scss @@ -0,0 +1,36 @@ +// +// Mixins: Scrollbar +// + +@mixin scrollbar-color-gray() { + scrollbar-color: var(--#{$prefix}secondary-bg) transparent; + + &::-webkit-scrollbar-thumb { + background-color: var(--#{$prefix}secondary-bg); + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-corner { + background-color: transparent; + } +} + +@mixin scrollbar-width-thin() { + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: .5rem; + height: .5rem; + } +} + +@mixin scrollbar-width-none() { + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 0; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_lockscreen.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_lockscreen.scss new file mode 100644 index 00000000..030e69fd --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_lockscreen.scss @@ -0,0 +1,75 @@ +// +// Pages: Lock Screen +// + +// ADD THIS CLASS TO THE TAG +.lockscreen { + // User name [optional] + .lockscreen-name { + font-weight: 600; + text-align: center; + } + + .lockscreen-logo { + margin-bottom: 25px; + font-size: 35px; + font-weight: 300; + text-align: center; + + a { + color: var(--#{$prefix}emphasis-color); + text-decoration: none; + } + } + + .lockscreen-wrapper { + max-width: 400px; + margin: 0 auto; + margin-top: 10%; + } + + // Will contain the image and the sign in form + .lockscreen-item { + position: relative; + width: 290px; + padding: 0; + margin: 10px auto 30px; + background-color: var(--#{$prefix}body-bg); + @include border-radius(4px); + } + + // User image + .lockscreen-image { + position: absolute; + top: -25px; + left: -10px; + z-index: 10; + padding: 5px; + background-color: var(--#{$prefix}body-bg); + @include border-radius(50%); + + > img { + @include border-radius(50%); + width: 70px; + height: 70px; + } + } + + // Contains the password input and the login button + .lockscreen-credentials { + margin-left: 70px; + + .form-control { + border: 0; + } + + .btn { + padding: 0 10px; + border: 0; + } + } + + .lockscreen-footer { + margin-top: 10px; + } +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_login_and_register.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_login_and_register.scss new file mode 100644 index 00000000..cc176307 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/pages/_login_and_register.scss @@ -0,0 +1,100 @@ +// +// Pages: Login & Register +// + +.login-logo, +.register-logo { + margin-bottom: .9rem; + font-size: 2.1rem; + font-weight: 300; + text-align: center; + + a { + color: var(--#{$prefix}secondary-color); + text-decoration: none; + } +} + +.login-page, +.register-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +.login-box, +.register-box { + width: 400px; + + @media (max-width: map-get($grid-breakpoints, sm)) { + width: 90%; + margin-top: .5rem; + } + + .card { + margin-bottom: 0; + } +} + +.login-card-body, +.register-card-body { + padding: 20px; + color: var(--#{$prefix}secondary-color); + background-color: var(--#{$prefix}body-bg); + border-top: 0; + + .input-group { + .form-control { + &:focus { + box-shadow: none; + + ~ .input-group-prepend .input-group-text, + ~ .input-group-append .input-group-text { + border-color: $input-focus-border-color; + } + } + + &.is-valid { + &:focus { + box-shadow: none; + } + + ~ .input-group-prepend .input-group-text, + ~ .input-group-append .input-group-text { + border-color: $success; + } + } + + &.is-invalid { + &:focus { + box-shadow: none; + } + + ~ .input-group-append .input-group-text { + border-color: $danger; + } + } + } + + .input-group-text { + color: var(--#{$prefix}secondary-color); + background-color: transparent; + @include border-top-end-radius($border-radius); + @include border-bottom-end-radius($border-radius); + @include transition($input-transition); + } + } +} + +.login-box-msg, +.register-box-msg { + padding: 0 20px 20px; + margin: 0; + text-align: center; +} + +.social-auth-links { + margin: 10px 0; +} diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_components.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_components.scss new file mode 100644 index 00000000..ec223fd9 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_components.scss @@ -0,0 +1,7 @@ +// +// Part: Components +// + +@import "../progress-bars"; +@import "../cards"; +@import "../table"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_core.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_core.scss new file mode 100644 index 00000000..255dc03d --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_core.scss @@ -0,0 +1,15 @@ +// +// Part: Core +// + +@import "../root"; +@import "../app-wrapper"; +@import "../app-content"; +@import "../app-header"; +@import "../app-sidebar"; +@import "../app-main"; +@import "../app-footer"; +@import "../dropdown"; +@import "../callouts"; +@import "../compact-mode"; +@import "../docs"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_extra-components.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_extra-components.scss new file mode 100644 index 00000000..1028da98 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_extra-components.scss @@ -0,0 +1,9 @@ +// +// Part: Extra Components +// + +@import "../small-box"; +@import "../info-box"; +@import "../timeline"; +@import "../direct-chat"; +@import "../toasts"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_miscellaneous.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_miscellaneous.scss new file mode 100644 index 00000000..cfdd705a --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_miscellaneous.scss @@ -0,0 +1,5 @@ +// +// Part: Miscellaneous +// + +@import "../miscellaneous"; diff --git a/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_pages.scss b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_pages.scss new file mode 100644 index 00000000..01cab17e --- /dev/null +++ b/extensions/pagetop-bootsier/assets/adminlte-4.0.0/scss/parts/_pages.scss @@ -0,0 +1,6 @@ +// +// Part: Pages +// + +@import "../pages/login_and_register"; +@import "../pages/lockscreen"; diff --git a/extensions/pagetop-bootsier/assets/bootsier.scss b/extensions/pagetop-bootsier/assets/bootsier.scss new file mode 100644 index 00000000..68b778fa --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootsier.scss @@ -0,0 +1,91 @@ +// AdminLTE v4.0.0 + Bootstrap 5.3.8 - PageTop Bootsier Styles. + +// Bootstrap Configuration +// --------------------------------------------------- +@import "bootstrap-5.3.8/scss/functions"; + +// AdminLTE Configuration (little modified are here) +// --------------------------------------------------- +@import "adminlte-4.0.0/scss/bootstrap-variables"; + +// 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"; + +// AdminLTE Configuration +// --------------------------------------------------- +@import "adminlte-4.0.0/scss/variables"; +@import "adminlte-4.0.0/scss/variables-dark"; +@import "adminlte-4.0.0/scss/mixins"; + +// AdminLTE Parts +// --------------------------------------------------- +@import "adminlte-4.0.0/scss/parts/core"; +@import "adminlte-4.0.0/scss/parts/components"; +@import "adminlte-4.0.0/scss/parts/extra-components"; +@import "adminlte-4.0.0/scss/parts/pages"; +@import "adminlte-4.0.0/scss/parts/miscellaneous"; + +// AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance +@import "adminlte-4.0.0/scss/accessibility"; + +// AdminLTE Documentation Styles (only applies to pages with body.docs-page) +@import "adminlte-4.0.0/scss/docs"; + +// Bootstrap Icons 1.13.1 +// --------------------------------------------------- +@import "bootsier-icons"; + +// Bootsier customizations +// --------------------------------------------------- +@import "bootsier-custom"; diff --git a/extensions/pagetop-bootsier/assets/bootsier.shell.js b/extensions/pagetop-bootsier/assets/bootsier.shell.js new file mode 100644 index 00000000..c2b6e2a5 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootsier.shell.js @@ -0,0 +1,65 @@ +(function () { + 'use strict'; + + // Fullscreen: keeps maximize/minimize icons in sync with the actual fullscreen state. + document.addEventListener('fullscreenchange', function () { + var isFs = !!document.fullscreenElement; + document.querySelectorAll('[data-lte-icon="maximize"]').forEach(function (el) { + el.classList.toggle('d-none', isFs); + }); + document.querySelectorAll('[data-lte-icon="minimize"]').forEach(function (el) { + el.classList.toggle('d-none', !isFs); + }); + }); + + // Color mode selector (light / dark / auto). + var STORAGE_KEY = 'lte-theme'; + var getStored = function () { return localStorage.getItem(STORAGE_KEY); }; + var setStored = function (theme) { localStorage.setItem(STORAGE_KEY, theme); }; + var prefersDark = function () { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + }; + + var setTheme = function (theme) { + var resolved = (theme === 'auto') ? (prefersDark() ? 'dark' : 'light') : theme; + document.documentElement.setAttribute('data-bs-theme', resolved); + }; + + var showActiveTheme = function (theme) { + document.querySelectorAll('[data-bs-theme-value]').forEach(function (el) { + el.classList.remove('active'); + el.setAttribute('aria-pressed', 'false'); + var check = el.querySelector('.bi-check-lg'); + if (check) { check.classList.add('d-none'); } + }); + var active = document.querySelector('[data-bs-theme-value="' + theme + '"]'); + if (active) { + active.classList.add('active'); + active.setAttribute('aria-pressed', 'true'); + var check = active.querySelector('.bi-check-lg'); + if (check) { check.classList.remove('d-none'); } + } + document.querySelectorAll('[data-lte-theme-icon]').forEach(function (icon) { + icon.classList.toggle('d-none', icon.dataset.lteThemeIcon !== theme); + }); + }; + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { + var stored = getStored(); + if (!stored || stored === 'auto') { setTheme('auto'); } + }); + + document.addEventListener('DOMContentLoaded', function () { + var theme = getStored() || 'auto'; + setTheme(theme); + showActiveTheme(theme); + document.querySelectorAll('[data-bs-theme-value]').forEach(function (toggle) { + toggle.addEventListener('click', function () { + var t = toggle.getAttribute('data-bs-theme-value'); + setStored(t); + setTheme(t); + showActiveTheme(t); + }); + }); + }); +}()); diff --git a/extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js similarity index 100% rename from extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js diff --git a/extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js.map b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js.map similarity index 100% rename from extensions/pagetop-bootsier/static/js/bootstrap.bundle.min.js.map rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js.map diff --git a/extensions/pagetop-bootsier/static/js/bootstrap.js b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.js similarity index 100% rename from extensions/pagetop-bootsier/static/js/bootstrap.js rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.js diff --git a/extensions/pagetop-bootsier/static/js/bootstrap.js.map b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.js.map similarity index 100% rename from extensions/pagetop-bootsier/static/js/bootstrap.js.map rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/js/bootstrap.js.map diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_accordion.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_accordion.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_accordion.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_accordion.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_alert.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_alert.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_alert.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_alert.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_badge.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_badge.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_badge.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_badge.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_breadcrumb.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_breadcrumb.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_breadcrumb.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_breadcrumb.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_button-group.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_button-group.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_button-group.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_button-group.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_buttons.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_buttons.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_buttons.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_buttons.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_card.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_card.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_card.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_card.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_carousel.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_carousel.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_carousel.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_carousel.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_close.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_close.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_close.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_close.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_containers.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_containers.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_containers.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_containers.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_dropdown.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_dropdown.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_dropdown.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_dropdown.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_forms.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_forms.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_forms.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_forms.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_functions.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_functions.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_functions.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_functions.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_grid.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_grid.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_grid.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_grid.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_helpers.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_helpers.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_helpers.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_helpers.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_images.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_images.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_images.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_images.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_list-group.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_list-group.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_list-group.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_list-group.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_maps.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_maps.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_maps.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_maps.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_mixins.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_mixins.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_mixins.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_mixins.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_modal.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_modal.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_modal.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_modal.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_nav.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_nav.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_nav.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_nav.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_navbar.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_navbar.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_navbar.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_navbar.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_offcanvas.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_offcanvas.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_offcanvas.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_offcanvas.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_pagination.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_pagination.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_pagination.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_pagination.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_placeholders.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_placeholders.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_placeholders.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_placeholders.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_popover.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_popover.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_popover.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_popover.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_progress.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_progress.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_progress.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_progress.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_reboot.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_reboot.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_reboot.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_reboot.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_root.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_root.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_root.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_root.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_spinners.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_spinners.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_spinners.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_spinners.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_tables.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_tables.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_tables.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_tables.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_toasts.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_toasts.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_toasts.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_toasts.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_tooltip.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_tooltip.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_tooltip.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_tooltip.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_transitions.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_transitions.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_transitions.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_transitions.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_type.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_type.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_type.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_type.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_utilities.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_utilities.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_utilities.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_utilities.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_variables-dark.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_variables-dark.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_variables-dark.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_variables-dark.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_variables.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_variables.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/_variables.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/_variables.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-grid.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-grid.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-grid.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-grid.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-reboot.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-reboot.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-reboot.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-reboot.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-utilities.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-utilities.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap-utilities.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap-utilities.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/bootstrap.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/bootstrap.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_floating-labels.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_floating-labels.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_floating-labels.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_floating-labels.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-check.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-check.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-check.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-check.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-control.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-control.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-control.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-control.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-range.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-range.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-range.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-range.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-select.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-select.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-select.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-select.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-text.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-text.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_form-text.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_form-text.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_input-group.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_input-group.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_input-group.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_input-group.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_labels.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_labels.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_labels.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_labels.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_validation.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_validation.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/forms/_validation.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/forms/_validation.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_clearfix.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_clearfix.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_clearfix.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_clearfix.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_color-bg.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_color-bg.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_color-bg.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_color-bg.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_colored-links.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_colored-links.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_colored-links.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_colored-links.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_focus-ring.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_focus-ring.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_focus-ring.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_focus-ring.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_icon-link.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_icon-link.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_icon-link.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_icon-link.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_position.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_position.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_position.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_position.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_ratio.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_ratio.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_ratio.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_ratio.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_stacks.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_stacks.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_stacks.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_stacks.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_stretched-link.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_stretched-link.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_stretched-link.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_stretched-link.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_text-truncation.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_text-truncation.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_text-truncation.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_text-truncation.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_visually-hidden.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_visually-hidden.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_visually-hidden.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_visually-hidden.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_vr.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_vr.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/helpers/_vr.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/helpers/_vr.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_alert.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_alert.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_alert.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_alert.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_backdrop.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_backdrop.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_backdrop.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_backdrop.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_banner.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_banner.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_banner.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_banner.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_border-radius.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_border-radius.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_border-radius.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_border-radius.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_box-shadow.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_box-shadow.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_box-shadow.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_box-shadow.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_breakpoints.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_breakpoints.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_breakpoints.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_breakpoints.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_buttons.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_buttons.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_buttons.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_buttons.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_caret.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_caret.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_caret.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_caret.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_clearfix.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_clearfix.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_clearfix.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_clearfix.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_color-mode.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_color-mode.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_color-mode.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_color-mode.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_color-scheme.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_color-scheme.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_color-scheme.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_color-scheme.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_container.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_container.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_container.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_container.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_deprecate.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_deprecate.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_deprecate.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_deprecate.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_forms.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_forms.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_forms.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_forms.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_gradients.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_gradients.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_gradients.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_gradients.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_grid.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_grid.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_grid.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_grid.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_image.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_image.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_image.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_image.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_list-group.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_list-group.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_list-group.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_list-group.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_lists.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_lists.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_lists.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_lists.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_pagination.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_pagination.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_pagination.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_pagination.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_reset-text.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_reset-text.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_reset-text.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_reset-text.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_resize.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_resize.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_resize.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_resize.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_table-variants.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_table-variants.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_table-variants.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_table-variants.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_text-truncate.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_text-truncate.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_text-truncate.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_text-truncate.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_transition.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_transition.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_transition.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_transition.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_utilities.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_utilities.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_utilities.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_utilities.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_visually-hidden.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_visually-hidden.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/mixins/_visually-hidden.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/mixins/_visually-hidden.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/jasmine.js b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/jasmine.js similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/jasmine.js rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/jasmine.js diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_auto-import-of-variables-dark.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_auto-import-of-variables-dark.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_auto-import-of-variables-dark.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_auto-import-of-variables-dark.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_box-shadow.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_box-shadow.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_box-shadow.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_box-shadow.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_color-contrast.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_color-contrast.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_color-contrast.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_color-contrast.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_color-modes.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_color-modes.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_color-modes.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_color-modes.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_media-query-color-mode-full.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_media-query-color-mode-full.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_media-query-color-mode-full.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_media-query-color-mode-full.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_utilities.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_utilities.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/mixins/_utilities.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/mixins/_utilities.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/sass-true/register.js b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/sass-true/register.js similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/sass-true/register.js rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/sass-true/register.js diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/sass-true/runner.js b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/sass-true/runner.js similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/sass-true/runner.js rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/sass-true/runner.js diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/utilities/_api.test.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/utilities/_api.test.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/tests/utilities/_api.test.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/tests/utilities/_api.test.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/utilities/_api.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/utilities/_api.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/utilities/_api.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/utilities/_api.scss diff --git a/extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/vendor/_rfs.scss b/extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/vendor/_rfs.scss similarity index 100% rename from extensions/pagetop-bootsier/static/scss/bootstrap-5.3.8/vendor/_rfs.scss rename to extensions/pagetop-bootsier/assets/bootstrap-5.3.8/scss/vendor/_rfs.scss diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.css b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.css new file mode 100644 index 00000000..5f7ae28e --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.css @@ -0,0 +1,2106 @@ +/*! + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */ + +@font-face { + font-display: block; + font-family: "bootstrap-icons"; + src: url("./fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"), +url("./fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } +.bi-arrow-through-heart-fill::before { content: "\f700"; } +.bi-arrow-through-heart::before { content: "\f701"; } +.bi-badge-sd-fill::before { content: "\f702"; } +.bi-badge-sd::before { content: "\f703"; } +.bi-bag-heart-fill::before { content: "\f704"; } +.bi-bag-heart::before { content: "\f705"; } +.bi-balloon-fill::before { content: "\f706"; } +.bi-balloon-heart-fill::before { content: "\f707"; } +.bi-balloon-heart::before { content: "\f708"; } +.bi-balloon::before { content: "\f709"; } +.bi-box2-fill::before { content: "\f70a"; } +.bi-box2-heart-fill::before { content: "\f70b"; } +.bi-box2-heart::before { content: "\f70c"; } +.bi-box2::before { content: "\f70d"; } +.bi-braces-asterisk::before { content: "\f70e"; } +.bi-calendar-heart-fill::before { content: "\f70f"; } +.bi-calendar-heart::before { content: "\f710"; } +.bi-calendar2-heart-fill::before { content: "\f711"; } +.bi-calendar2-heart::before { content: "\f712"; } +.bi-chat-heart-fill::before { content: "\f713"; } +.bi-chat-heart::before { content: "\f714"; } +.bi-chat-left-heart-fill::before { content: "\f715"; } +.bi-chat-left-heart::before { content: "\f716"; } +.bi-chat-right-heart-fill::before { content: "\f717"; } +.bi-chat-right-heart::before { content: "\f718"; } +.bi-chat-square-heart-fill::before { content: "\f719"; } +.bi-chat-square-heart::before { content: "\f71a"; } +.bi-clipboard-check-fill::before { content: "\f71b"; } +.bi-clipboard-data-fill::before { content: "\f71c"; } +.bi-clipboard-fill::before { content: "\f71d"; } +.bi-clipboard-heart-fill::before { content: "\f71e"; } +.bi-clipboard-heart::before { content: "\f71f"; } +.bi-clipboard-minus-fill::before { content: "\f720"; } +.bi-clipboard-plus-fill::before { content: "\f721"; } +.bi-clipboard-pulse::before { content: "\f722"; } +.bi-clipboard-x-fill::before { content: "\f723"; } +.bi-clipboard2-check-fill::before { content: "\f724"; } +.bi-clipboard2-check::before { content: "\f725"; } +.bi-clipboard2-data-fill::before { content: "\f726"; } +.bi-clipboard2-data::before { content: "\f727"; } +.bi-clipboard2-fill::before { content: "\f728"; } +.bi-clipboard2-heart-fill::before { content: "\f729"; } +.bi-clipboard2-heart::before { content: "\f72a"; } +.bi-clipboard2-minus-fill::before { content: "\f72b"; } +.bi-clipboard2-minus::before { content: "\f72c"; } +.bi-clipboard2-plus-fill::before { content: "\f72d"; } +.bi-clipboard2-plus::before { content: "\f72e"; } +.bi-clipboard2-pulse-fill::before { content: "\f72f"; } +.bi-clipboard2-pulse::before { content: "\f730"; } +.bi-clipboard2-x-fill::before { content: "\f731"; } +.bi-clipboard2-x::before { content: "\f732"; } +.bi-clipboard2::before { content: "\f733"; } +.bi-emoji-kiss-fill::before { content: "\f734"; } +.bi-emoji-kiss::before { content: "\f735"; } +.bi-envelope-heart-fill::before { content: "\f736"; } +.bi-envelope-heart::before { content: "\f737"; } +.bi-envelope-open-heart-fill::before { content: "\f738"; } +.bi-envelope-open-heart::before { content: "\f739"; } +.bi-envelope-paper-fill::before { content: "\f73a"; } +.bi-envelope-paper-heart-fill::before { content: "\f73b"; } +.bi-envelope-paper-heart::before { content: "\f73c"; } +.bi-envelope-paper::before { content: "\f73d"; } +.bi-filetype-aac::before { content: "\f73e"; } +.bi-filetype-ai::before { content: "\f73f"; } +.bi-filetype-bmp::before { content: "\f740"; } +.bi-filetype-cs::before { content: "\f741"; } +.bi-filetype-css::before { content: "\f742"; } +.bi-filetype-csv::before { content: "\f743"; } +.bi-filetype-doc::before { content: "\f744"; } +.bi-filetype-docx::before { content: "\f745"; } +.bi-filetype-exe::before { content: "\f746"; } +.bi-filetype-gif::before { content: "\f747"; } +.bi-filetype-heic::before { content: "\f748"; } +.bi-filetype-html::before { content: "\f749"; } +.bi-filetype-java::before { content: "\f74a"; } +.bi-filetype-jpg::before { content: "\f74b"; } +.bi-filetype-js::before { content: "\f74c"; } +.bi-filetype-jsx::before { content: "\f74d"; } +.bi-filetype-key::before { content: "\f74e"; } +.bi-filetype-m4p::before { content: "\f74f"; } +.bi-filetype-md::before { content: "\f750"; } +.bi-filetype-mdx::before { content: "\f751"; } +.bi-filetype-mov::before { content: "\f752"; } +.bi-filetype-mp3::before { content: "\f753"; } +.bi-filetype-mp4::before { content: "\f754"; } +.bi-filetype-otf::before { content: "\f755"; } +.bi-filetype-pdf::before { content: "\f756"; } +.bi-filetype-php::before { content: "\f757"; } +.bi-filetype-png::before { content: "\f758"; } +.bi-filetype-ppt::before { content: "\f75a"; } +.bi-filetype-psd::before { content: "\f75b"; } +.bi-filetype-py::before { content: "\f75c"; } +.bi-filetype-raw::before { content: "\f75d"; } +.bi-filetype-rb::before { content: "\f75e"; } +.bi-filetype-sass::before { content: "\f75f"; } +.bi-filetype-scss::before { content: "\f760"; } +.bi-filetype-sh::before { content: "\f761"; } +.bi-filetype-svg::before { content: "\f762"; } +.bi-filetype-tiff::before { content: "\f763"; } +.bi-filetype-tsx::before { content: "\f764"; } +.bi-filetype-ttf::before { content: "\f765"; } +.bi-filetype-txt::before { content: "\f766"; } +.bi-filetype-wav::before { content: "\f767"; } +.bi-filetype-woff::before { content: "\f768"; } +.bi-filetype-xls::before { content: "\f76a"; } +.bi-filetype-xml::before { content: "\f76b"; } +.bi-filetype-yml::before { content: "\f76c"; } +.bi-heart-arrow::before { content: "\f76d"; } +.bi-heart-pulse-fill::before { content: "\f76e"; } +.bi-heart-pulse::before { content: "\f76f"; } +.bi-heartbreak-fill::before { content: "\f770"; } +.bi-heartbreak::before { content: "\f771"; } +.bi-hearts::before { content: "\f772"; } +.bi-hospital-fill::before { content: "\f773"; } +.bi-hospital::before { content: "\f774"; } +.bi-house-heart-fill::before { content: "\f775"; } +.bi-house-heart::before { content: "\f776"; } +.bi-incognito::before { content: "\f777"; } +.bi-magnet-fill::before { content: "\f778"; } +.bi-magnet::before { content: "\f779"; } +.bi-person-heart::before { content: "\f77a"; } +.bi-person-hearts::before { content: "\f77b"; } +.bi-phone-flip::before { content: "\f77c"; } +.bi-plugin::before { content: "\f77d"; } +.bi-postage-fill::before { content: "\f77e"; } +.bi-postage-heart-fill::before { content: "\f77f"; } +.bi-postage-heart::before { content: "\f780"; } +.bi-postage::before { content: "\f781"; } +.bi-postcard-fill::before { content: "\f782"; } +.bi-postcard-heart-fill::before { content: "\f783"; } +.bi-postcard-heart::before { content: "\f784"; } +.bi-postcard::before { content: "\f785"; } +.bi-search-heart-fill::before { content: "\f786"; } +.bi-search-heart::before { content: "\f787"; } +.bi-sliders2-vertical::before { content: "\f788"; } +.bi-sliders2::before { content: "\f789"; } +.bi-trash3-fill::before { content: "\f78a"; } +.bi-trash3::before { content: "\f78b"; } +.bi-valentine::before { content: "\f78c"; } +.bi-valentine2::before { content: "\f78d"; } +.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; } +.bi-wrench-adjustable-circle::before { content: "\f78f"; } +.bi-wrench-adjustable::before { content: "\f790"; } +.bi-filetype-json::before { content: "\f791"; } +.bi-filetype-pptx::before { content: "\f792"; } +.bi-filetype-xlsx::before { content: "\f793"; } +.bi-1-circle-fill::before { content: "\f796"; } +.bi-1-circle::before { content: "\f797"; } +.bi-1-square-fill::before { content: "\f798"; } +.bi-1-square::before { content: "\f799"; } +.bi-2-circle-fill::before { content: "\f79c"; } +.bi-2-circle::before { content: "\f79d"; } +.bi-2-square-fill::before { content: "\f79e"; } +.bi-2-square::before { content: "\f79f"; } +.bi-3-circle-fill::before { content: "\f7a2"; } +.bi-3-circle::before { content: "\f7a3"; } +.bi-3-square-fill::before { content: "\f7a4"; } +.bi-3-square::before { content: "\f7a5"; } +.bi-4-circle-fill::before { content: "\f7a8"; } +.bi-4-circle::before { content: "\f7a9"; } +.bi-4-square-fill::before { content: "\f7aa"; } +.bi-4-square::before { content: "\f7ab"; } +.bi-5-circle-fill::before { content: "\f7ae"; } +.bi-5-circle::before { content: "\f7af"; } +.bi-5-square-fill::before { content: "\f7b0"; } +.bi-5-square::before { content: "\f7b1"; } +.bi-6-circle-fill::before { content: "\f7b4"; } +.bi-6-circle::before { content: "\f7b5"; } +.bi-6-square-fill::before { content: "\f7b6"; } +.bi-6-square::before { content: "\f7b7"; } +.bi-7-circle-fill::before { content: "\f7ba"; } +.bi-7-circle::before { content: "\f7bb"; } +.bi-7-square-fill::before { content: "\f7bc"; } +.bi-7-square::before { content: "\f7bd"; } +.bi-8-circle-fill::before { content: "\f7c0"; } +.bi-8-circle::before { content: "\f7c1"; } +.bi-8-square-fill::before { content: "\f7c2"; } +.bi-8-square::before { content: "\f7c3"; } +.bi-9-circle-fill::before { content: "\f7c6"; } +.bi-9-circle::before { content: "\f7c7"; } +.bi-9-square-fill::before { content: "\f7c8"; } +.bi-9-square::before { content: "\f7c9"; } +.bi-airplane-engines-fill::before { content: "\f7ca"; } +.bi-airplane-engines::before { content: "\f7cb"; } +.bi-airplane-fill::before { content: "\f7cc"; } +.bi-airplane::before { content: "\f7cd"; } +.bi-alexa::before { content: "\f7ce"; } +.bi-alipay::before { content: "\f7cf"; } +.bi-android::before { content: "\f7d0"; } +.bi-android2::before { content: "\f7d1"; } +.bi-box-fill::before { content: "\f7d2"; } +.bi-box-seam-fill::before { content: "\f7d3"; } +.bi-browser-chrome::before { content: "\f7d4"; } +.bi-browser-edge::before { content: "\f7d5"; } +.bi-browser-firefox::before { content: "\f7d6"; } +.bi-browser-safari::before { content: "\f7d7"; } +.bi-c-circle-fill::before { content: "\f7da"; } +.bi-c-circle::before { content: "\f7db"; } +.bi-c-square-fill::before { content: "\f7dc"; } +.bi-c-square::before { content: "\f7dd"; } +.bi-capsule-pill::before { content: "\f7de"; } +.bi-capsule::before { content: "\f7df"; } +.bi-car-front-fill::before { content: "\f7e0"; } +.bi-car-front::before { content: "\f7e1"; } +.bi-cassette-fill::before { content: "\f7e2"; } +.bi-cassette::before { content: "\f7e3"; } +.bi-cc-circle-fill::before { content: "\f7e6"; } +.bi-cc-circle::before { content: "\f7e7"; } +.bi-cc-square-fill::before { content: "\f7e8"; } +.bi-cc-square::before { content: "\f7e9"; } +.bi-cup-hot-fill::before { content: "\f7ea"; } +.bi-cup-hot::before { content: "\f7eb"; } +.bi-currency-rupee::before { content: "\f7ec"; } +.bi-dropbox::before { content: "\f7ed"; } +.bi-escape::before { content: "\f7ee"; } +.bi-fast-forward-btn-fill::before { content: "\f7ef"; } +.bi-fast-forward-btn::before { content: "\f7f0"; } +.bi-fast-forward-circle-fill::before { content: "\f7f1"; } +.bi-fast-forward-circle::before { content: "\f7f2"; } +.bi-fast-forward-fill::before { content: "\f7f3"; } +.bi-fast-forward::before { content: "\f7f4"; } +.bi-filetype-sql::before { content: "\f7f5"; } +.bi-fire::before { content: "\f7f6"; } +.bi-google-play::before { content: "\f7f7"; } +.bi-h-circle-fill::before { content: "\f7fa"; } +.bi-h-circle::before { content: "\f7fb"; } +.bi-h-square-fill::before { content: "\f7fc"; } +.bi-h-square::before { content: "\f7fd"; } +.bi-indent::before { content: "\f7fe"; } +.bi-lungs-fill::before { content: "\f7ff"; } +.bi-lungs::before { content: "\f800"; } +.bi-microsoft-teams::before { content: "\f801"; } +.bi-p-circle-fill::before { content: "\f804"; } +.bi-p-circle::before { content: "\f805"; } +.bi-p-square-fill::before { content: "\f806"; } +.bi-p-square::before { content: "\f807"; } +.bi-pass-fill::before { content: "\f808"; } +.bi-pass::before { content: "\f809"; } +.bi-prescription::before { content: "\f80a"; } +.bi-prescription2::before { content: "\f80b"; } +.bi-r-circle-fill::before { content: "\f80e"; } +.bi-r-circle::before { content: "\f80f"; } +.bi-r-square-fill::before { content: "\f810"; } +.bi-r-square::before { content: "\f811"; } +.bi-repeat-1::before { content: "\f812"; } +.bi-repeat::before { content: "\f813"; } +.bi-rewind-btn-fill::before { content: "\f814"; } +.bi-rewind-btn::before { content: "\f815"; } +.bi-rewind-circle-fill::before { content: "\f816"; } +.bi-rewind-circle::before { content: "\f817"; } +.bi-rewind-fill::before { content: "\f818"; } +.bi-rewind::before { content: "\f819"; } +.bi-train-freight-front-fill::before { content: "\f81a"; } +.bi-train-freight-front::before { content: "\f81b"; } +.bi-train-front-fill::before { content: "\f81c"; } +.bi-train-front::before { content: "\f81d"; } +.bi-train-lightrail-front-fill::before { content: "\f81e"; } +.bi-train-lightrail-front::before { content: "\f81f"; } +.bi-truck-front-fill::before { content: "\f820"; } +.bi-truck-front::before { content: "\f821"; } +.bi-ubuntu::before { content: "\f822"; } +.bi-unindent::before { content: "\f823"; } +.bi-unity::before { content: "\f824"; } +.bi-universal-access-circle::before { content: "\f825"; } +.bi-universal-access::before { content: "\f826"; } +.bi-virus::before { content: "\f827"; } +.bi-virus2::before { content: "\f828"; } +.bi-wechat::before { content: "\f829"; } +.bi-yelp::before { content: "\f82a"; } +.bi-sign-stop-fill::before { content: "\f82b"; } +.bi-sign-stop-lights-fill::before { content: "\f82c"; } +.bi-sign-stop-lights::before { content: "\f82d"; } +.bi-sign-stop::before { content: "\f82e"; } +.bi-sign-turn-left-fill::before { content: "\f82f"; } +.bi-sign-turn-left::before { content: "\f830"; } +.bi-sign-turn-right-fill::before { content: "\f831"; } +.bi-sign-turn-right::before { content: "\f832"; } +.bi-sign-turn-slight-left-fill::before { content: "\f833"; } +.bi-sign-turn-slight-left::before { content: "\f834"; } +.bi-sign-turn-slight-right-fill::before { content: "\f835"; } +.bi-sign-turn-slight-right::before { content: "\f836"; } +.bi-sign-yield-fill::before { content: "\f837"; } +.bi-sign-yield::before { content: "\f838"; } +.bi-ev-station-fill::before { content: "\f839"; } +.bi-ev-station::before { content: "\f83a"; } +.bi-fuel-pump-diesel-fill::before { content: "\f83b"; } +.bi-fuel-pump-diesel::before { content: "\f83c"; } +.bi-fuel-pump-fill::before { content: "\f83d"; } +.bi-fuel-pump::before { content: "\f83e"; } +.bi-0-circle-fill::before { content: "\f83f"; } +.bi-0-circle::before { content: "\f840"; } +.bi-0-square-fill::before { content: "\f841"; } +.bi-0-square::before { content: "\f842"; } +.bi-rocket-fill::before { content: "\f843"; } +.bi-rocket-takeoff-fill::before { content: "\f844"; } +.bi-rocket-takeoff::before { content: "\f845"; } +.bi-rocket::before { content: "\f846"; } +.bi-stripe::before { content: "\f847"; } +.bi-subscript::before { content: "\f848"; } +.bi-superscript::before { content: "\f849"; } +.bi-trello::before { content: "\f84a"; } +.bi-envelope-at-fill::before { content: "\f84b"; } +.bi-envelope-at::before { content: "\f84c"; } +.bi-regex::before { content: "\f84d"; } +.bi-text-wrap::before { content: "\f84e"; } +.bi-sign-dead-end-fill::before { content: "\f84f"; } +.bi-sign-dead-end::before { content: "\f850"; } +.bi-sign-do-not-enter-fill::before { content: "\f851"; } +.bi-sign-do-not-enter::before { content: "\f852"; } +.bi-sign-intersection-fill::before { content: "\f853"; } +.bi-sign-intersection-side-fill::before { content: "\f854"; } +.bi-sign-intersection-side::before { content: "\f855"; } +.bi-sign-intersection-t-fill::before { content: "\f856"; } +.bi-sign-intersection-t::before { content: "\f857"; } +.bi-sign-intersection-y-fill::before { content: "\f858"; } +.bi-sign-intersection-y::before { content: "\f859"; } +.bi-sign-intersection::before { content: "\f85a"; } +.bi-sign-merge-left-fill::before { content: "\f85b"; } +.bi-sign-merge-left::before { content: "\f85c"; } +.bi-sign-merge-right-fill::before { content: "\f85d"; } +.bi-sign-merge-right::before { content: "\f85e"; } +.bi-sign-no-left-turn-fill::before { content: "\f85f"; } +.bi-sign-no-left-turn::before { content: "\f860"; } +.bi-sign-no-parking-fill::before { content: "\f861"; } +.bi-sign-no-parking::before { content: "\f862"; } +.bi-sign-no-right-turn-fill::before { content: "\f863"; } +.bi-sign-no-right-turn::before { content: "\f864"; } +.bi-sign-railroad-fill::before { content: "\f865"; } +.bi-sign-railroad::before { content: "\f866"; } +.bi-building-add::before { content: "\f867"; } +.bi-building-check::before { content: "\f868"; } +.bi-building-dash::before { content: "\f869"; } +.bi-building-down::before { content: "\f86a"; } +.bi-building-exclamation::before { content: "\f86b"; } +.bi-building-fill-add::before { content: "\f86c"; } +.bi-building-fill-check::before { content: "\f86d"; } +.bi-building-fill-dash::before { content: "\f86e"; } +.bi-building-fill-down::before { content: "\f86f"; } +.bi-building-fill-exclamation::before { content: "\f870"; } +.bi-building-fill-gear::before { content: "\f871"; } +.bi-building-fill-lock::before { content: "\f872"; } +.bi-building-fill-slash::before { content: "\f873"; } +.bi-building-fill-up::before { content: "\f874"; } +.bi-building-fill-x::before { content: "\f875"; } +.bi-building-fill::before { content: "\f876"; } +.bi-building-gear::before { content: "\f877"; } +.bi-building-lock::before { content: "\f878"; } +.bi-building-slash::before { content: "\f879"; } +.bi-building-up::before { content: "\f87a"; } +.bi-building-x::before { content: "\f87b"; } +.bi-buildings-fill::before { content: "\f87c"; } +.bi-buildings::before { content: "\f87d"; } +.bi-bus-front-fill::before { content: "\f87e"; } +.bi-bus-front::before { content: "\f87f"; } +.bi-ev-front-fill::before { content: "\f880"; } +.bi-ev-front::before { content: "\f881"; } +.bi-globe-americas::before { content: "\f882"; } +.bi-globe-asia-australia::before { content: "\f883"; } +.bi-globe-central-south-asia::before { content: "\f884"; } +.bi-globe-europe-africa::before { content: "\f885"; } +.bi-house-add-fill::before { content: "\f886"; } +.bi-house-add::before { content: "\f887"; } +.bi-house-check-fill::before { content: "\f888"; } +.bi-house-check::before { content: "\f889"; } +.bi-house-dash-fill::before { content: "\f88a"; } +.bi-house-dash::before { content: "\f88b"; } +.bi-house-down-fill::before { content: "\f88c"; } +.bi-house-down::before { content: "\f88d"; } +.bi-house-exclamation-fill::before { content: "\f88e"; } +.bi-house-exclamation::before { content: "\f88f"; } +.bi-house-gear-fill::before { content: "\f890"; } +.bi-house-gear::before { content: "\f891"; } +.bi-house-lock-fill::before { content: "\f892"; } +.bi-house-lock::before { content: "\f893"; } +.bi-house-slash-fill::before { content: "\f894"; } +.bi-house-slash::before { content: "\f895"; } +.bi-house-up-fill::before { content: "\f896"; } +.bi-house-up::before { content: "\f897"; } +.bi-house-x-fill::before { content: "\f898"; } +.bi-house-x::before { content: "\f899"; } +.bi-person-add::before { content: "\f89a"; } +.bi-person-down::before { content: "\f89b"; } +.bi-person-exclamation::before { content: "\f89c"; } +.bi-person-fill-add::before { content: "\f89d"; } +.bi-person-fill-check::before { content: "\f89e"; } +.bi-person-fill-dash::before { content: "\f89f"; } +.bi-person-fill-down::before { content: "\f8a0"; } +.bi-person-fill-exclamation::before { content: "\f8a1"; } +.bi-person-fill-gear::before { content: "\f8a2"; } +.bi-person-fill-lock::before { content: "\f8a3"; } +.bi-person-fill-slash::before { content: "\f8a4"; } +.bi-person-fill-up::before { content: "\f8a5"; } +.bi-person-fill-x::before { content: "\f8a6"; } +.bi-person-gear::before { content: "\f8a7"; } +.bi-person-lock::before { content: "\f8a8"; } +.bi-person-slash::before { content: "\f8a9"; } +.bi-person-up::before { content: "\f8aa"; } +.bi-scooter::before { content: "\f8ab"; } +.bi-taxi-front-fill::before { content: "\f8ac"; } +.bi-taxi-front::before { content: "\f8ad"; } +.bi-amd::before { content: "\f8ae"; } +.bi-database-add::before { content: "\f8af"; } +.bi-database-check::before { content: "\f8b0"; } +.bi-database-dash::before { content: "\f8b1"; } +.bi-database-down::before { content: "\f8b2"; } +.bi-database-exclamation::before { content: "\f8b3"; } +.bi-database-fill-add::before { content: "\f8b4"; } +.bi-database-fill-check::before { content: "\f8b5"; } +.bi-database-fill-dash::before { content: "\f8b6"; } +.bi-database-fill-down::before { content: "\f8b7"; } +.bi-database-fill-exclamation::before { content: "\f8b8"; } +.bi-database-fill-gear::before { content: "\f8b9"; } +.bi-database-fill-lock::before { content: "\f8ba"; } +.bi-database-fill-slash::before { content: "\f8bb"; } +.bi-database-fill-up::before { content: "\f8bc"; } +.bi-database-fill-x::before { content: "\f8bd"; } +.bi-database-fill::before { content: "\f8be"; } +.bi-database-gear::before { content: "\f8bf"; } +.bi-database-lock::before { content: "\f8c0"; } +.bi-database-slash::before { content: "\f8c1"; } +.bi-database-up::before { content: "\f8c2"; } +.bi-database-x::before { content: "\f8c3"; } +.bi-database::before { content: "\f8c4"; } +.bi-houses-fill::before { content: "\f8c5"; } +.bi-houses::before { content: "\f8c6"; } +.bi-nvidia::before { content: "\f8c7"; } +.bi-person-vcard-fill::before { content: "\f8c8"; } +.bi-person-vcard::before { content: "\f8c9"; } +.bi-sina-weibo::before { content: "\f8ca"; } +.bi-tencent-qq::before { content: "\f8cb"; } +.bi-wikipedia::before { content: "\f8cc"; } +.bi-alphabet-uppercase::before { content: "\f2a5"; } +.bi-alphabet::before { content: "\f68a"; } +.bi-amazon::before { content: "\f68d"; } +.bi-arrows-collapse-vertical::before { content: "\f690"; } +.bi-arrows-expand-vertical::before { content: "\f695"; } +.bi-arrows-vertical::before { content: "\f698"; } +.bi-arrows::before { content: "\f6a2"; } +.bi-ban-fill::before { content: "\f6a3"; } +.bi-ban::before { content: "\f6b6"; } +.bi-bing::before { content: "\f6c2"; } +.bi-cake::before { content: "\f6e0"; } +.bi-cake2::before { content: "\f6ed"; } +.bi-cookie::before { content: "\f6ee"; } +.bi-copy::before { content: "\f759"; } +.bi-crosshair::before { content: "\f769"; } +.bi-crosshair2::before { content: "\f794"; } +.bi-emoji-astonished-fill::before { content: "\f795"; } +.bi-emoji-astonished::before { content: "\f79a"; } +.bi-emoji-grimace-fill::before { content: "\f79b"; } +.bi-emoji-grimace::before { content: "\f7a0"; } +.bi-emoji-grin-fill::before { content: "\f7a1"; } +.bi-emoji-grin::before { content: "\f7a6"; } +.bi-emoji-surprise-fill::before { content: "\f7a7"; } +.bi-emoji-surprise::before { content: "\f7ac"; } +.bi-emoji-tear-fill::before { content: "\f7ad"; } +.bi-emoji-tear::before { content: "\f7b2"; } +.bi-envelope-arrow-down-fill::before { content: "\f7b3"; } +.bi-envelope-arrow-down::before { content: "\f7b8"; } +.bi-envelope-arrow-up-fill::before { content: "\f7b9"; } +.bi-envelope-arrow-up::before { content: "\f7be"; } +.bi-feather::before { content: "\f7bf"; } +.bi-feather2::before { content: "\f7c4"; } +.bi-floppy-fill::before { content: "\f7c5"; } +.bi-floppy::before { content: "\f7d8"; } +.bi-floppy2-fill::before { content: "\f7d9"; } +.bi-floppy2::before { content: "\f7e4"; } +.bi-gitlab::before { content: "\f7e5"; } +.bi-highlighter::before { content: "\f7f8"; } +.bi-marker-tip::before { content: "\f802"; } +.bi-nvme-fill::before { content: "\f803"; } +.bi-nvme::before { content: "\f80c"; } +.bi-opencollective::before { content: "\f80d"; } +.bi-pci-card-network::before { content: "\f8cd"; } +.bi-pci-card-sound::before { content: "\f8ce"; } +.bi-radar::before { content: "\f8cf"; } +.bi-send-arrow-down-fill::before { content: "\f8d0"; } +.bi-send-arrow-down::before { content: "\f8d1"; } +.bi-send-arrow-up-fill::before { content: "\f8d2"; } +.bi-send-arrow-up::before { content: "\f8d3"; } +.bi-sim-slash-fill::before { content: "\f8d4"; } +.bi-sim-slash::before { content: "\f8d5"; } +.bi-sourceforge::before { content: "\f8d6"; } +.bi-substack::before { content: "\f8d7"; } +.bi-threads-fill::before { content: "\f8d8"; } +.bi-threads::before { content: "\f8d9"; } +.bi-transparency::before { content: "\f8da"; } +.bi-twitter-x::before { content: "\f8db"; } +.bi-type-h4::before { content: "\f8dc"; } +.bi-type-h5::before { content: "\f8dd"; } +.bi-type-h6::before { content: "\f8de"; } +.bi-backpack-fill::before { content: "\f8df"; } +.bi-backpack::before { content: "\f8e0"; } +.bi-backpack2-fill::before { content: "\f8e1"; } +.bi-backpack2::before { content: "\f8e2"; } +.bi-backpack3-fill::before { content: "\f8e3"; } +.bi-backpack3::before { content: "\f8e4"; } +.bi-backpack4-fill::before { content: "\f8e5"; } +.bi-backpack4::before { content: "\f8e6"; } +.bi-brilliance::before { content: "\f8e7"; } +.bi-cake-fill::before { content: "\f8e8"; } +.bi-cake2-fill::before { content: "\f8e9"; } +.bi-duffle-fill::before { content: "\f8ea"; } +.bi-duffle::before { content: "\f8eb"; } +.bi-exposure::before { content: "\f8ec"; } +.bi-gender-neuter::before { content: "\f8ed"; } +.bi-highlights::before { content: "\f8ee"; } +.bi-luggage-fill::before { content: "\f8ef"; } +.bi-luggage::before { content: "\f8f0"; } +.bi-mailbox-flag::before { content: "\f8f1"; } +.bi-mailbox2-flag::before { content: "\f8f2"; } +.bi-noise-reduction::before { content: "\f8f3"; } +.bi-passport-fill::before { content: "\f8f4"; } +.bi-passport::before { content: "\f8f5"; } +.bi-person-arms-up::before { content: "\f8f6"; } +.bi-person-raised-hand::before { content: "\f8f7"; } +.bi-person-standing-dress::before { content: "\f8f8"; } +.bi-person-standing::before { content: "\f8f9"; } +.bi-person-walking::before { content: "\f8fa"; } +.bi-person-wheelchair::before { content: "\f8fb"; } +.bi-shadows::before { content: "\f8fc"; } +.bi-suitcase-fill::before { content: "\f8fd"; } +.bi-suitcase-lg-fill::before { content: "\f8fe"; } +.bi-suitcase-lg::before { content: "\f8ff"; } +.bi-suitcase::before { content: "\f900"; } +.bi-suitcase2-fill::before { content: "\f901"; } +.bi-suitcase2::before { content: "\f902"; } +.bi-vignette::before { content: "\f903"; } +.bi-bluesky::before { content: "\f7f9"; } +.bi-tux::before { content: "\f904"; } +.bi-beaker-fill::before { content: "\f905"; } +.bi-beaker::before { content: "\f906"; } +.bi-flask-fill::before { content: "\f907"; } +.bi-flask-florence-fill::before { content: "\f908"; } +.bi-flask-florence::before { content: "\f909"; } +.bi-flask::before { content: "\f90a"; } +.bi-leaf-fill::before { content: "\f90b"; } +.bi-leaf::before { content: "\f90c"; } +.bi-measuring-cup-fill::before { content: "\f90d"; } +.bi-measuring-cup::before { content: "\f90e"; } +.bi-unlock2-fill::before { content: "\f90f"; } +.bi-unlock2::before { content: "\f910"; } +.bi-battery-low::before { content: "\f911"; } +.bi-anthropic::before { content: "\f912"; } +.bi-apple-music::before { content: "\f913"; } +.bi-claude::before { content: "\f914"; } +.bi-openai::before { content: "\f915"; } +.bi-perplexity::before { content: "\f916"; } +.bi-css::before { content: "\f917"; } +.bi-javascript::before { content: "\f918"; } +.bi-typescript::before { content: "\f919"; } +.bi-fork-knife::before { content: "\f91a"; } +.bi-globe-americas-fill::before { content: "\f91b"; } +.bi-globe-asia-australia-fill::before { content: "\f91c"; } +.bi-globe-central-south-asia-fill::before { content: "\f91d"; } +.bi-globe-europe-africa-fill::before { content: "\f91e"; } diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.json b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.json new file mode 100644 index 00000000..9d8873b1 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.json @@ -0,0 +1,2080 @@ +{ + "123": 63103, + "alarm-fill": 61697, + "alarm": 61698, + "align-bottom": 61699, + "align-center": 61700, + "align-end": 61701, + "align-middle": 61702, + "align-start": 61703, + "align-top": 61704, + "alt": 61705, + "app-indicator": 61706, + "app": 61707, + "archive-fill": 61708, + "archive": 61709, + "arrow-90deg-down": 61710, + "arrow-90deg-left": 61711, + "arrow-90deg-right": 61712, + "arrow-90deg-up": 61713, + "arrow-bar-down": 61714, + "arrow-bar-left": 61715, + "arrow-bar-right": 61716, + "arrow-bar-up": 61717, + "arrow-clockwise": 61718, + "arrow-counterclockwise": 61719, + "arrow-down-circle-fill": 61720, + "arrow-down-circle": 61721, + "arrow-down-left-circle-fill": 61722, + "arrow-down-left-circle": 61723, + "arrow-down-left-square-fill": 61724, + "arrow-down-left-square": 61725, + "arrow-down-left": 61726, + "arrow-down-right-circle-fill": 61727, + "arrow-down-right-circle": 61728, + "arrow-down-right-square-fill": 61729, + "arrow-down-right-square": 61730, + "arrow-down-right": 61731, + "arrow-down-short": 61732, + "arrow-down-square-fill": 61733, + "arrow-down-square": 61734, + "arrow-down-up": 61735, + "arrow-down": 61736, + "arrow-left-circle-fill": 61737, + "arrow-left-circle": 61738, + "arrow-left-right": 61739, + "arrow-left-short": 61740, + "arrow-left-square-fill": 61741, + "arrow-left-square": 61742, + "arrow-left": 61743, + "arrow-repeat": 61744, + "arrow-return-left": 61745, + "arrow-return-right": 61746, + "arrow-right-circle-fill": 61747, + "arrow-right-circle": 61748, + "arrow-right-short": 61749, + "arrow-right-square-fill": 61750, + "arrow-right-square": 61751, + "arrow-right": 61752, + "arrow-up-circle-fill": 61753, + "arrow-up-circle": 61754, + "arrow-up-left-circle-fill": 61755, + "arrow-up-left-circle": 61756, + "arrow-up-left-square-fill": 61757, + "arrow-up-left-square": 61758, + "arrow-up-left": 61759, + "arrow-up-right-circle-fill": 61760, + "arrow-up-right-circle": 61761, + "arrow-up-right-square-fill": 61762, + "arrow-up-right-square": 61763, + "arrow-up-right": 61764, + "arrow-up-short": 61765, + "arrow-up-square-fill": 61766, + "arrow-up-square": 61767, + "arrow-up": 61768, + "arrows-angle-contract": 61769, + "arrows-angle-expand": 61770, + "arrows-collapse": 61771, + "arrows-expand": 61772, + "arrows-fullscreen": 61773, + "arrows-move": 61774, + "aspect-ratio-fill": 61775, + "aspect-ratio": 61776, + "asterisk": 61777, + "at": 61778, + "award-fill": 61779, + "award": 61780, + "back": 61781, + "backspace-fill": 61782, + "backspace-reverse-fill": 61783, + "backspace-reverse": 61784, + "backspace": 61785, + "badge-3d-fill": 61786, + "badge-3d": 61787, + "badge-4k-fill": 61788, + "badge-4k": 61789, + "badge-8k-fill": 61790, + "badge-8k": 61791, + "badge-ad-fill": 61792, + "badge-ad": 61793, + "badge-ar-fill": 61794, + "badge-ar": 61795, + "badge-cc-fill": 61796, + "badge-cc": 61797, + "badge-hd-fill": 61798, + "badge-hd": 61799, + "badge-tm-fill": 61800, + "badge-tm": 61801, + "badge-vo-fill": 61802, + "badge-vo": 61803, + "badge-vr-fill": 61804, + "badge-vr": 61805, + "badge-wc-fill": 61806, + "badge-wc": 61807, + "bag-check-fill": 61808, + "bag-check": 61809, + "bag-dash-fill": 61810, + "bag-dash": 61811, + "bag-fill": 61812, + "bag-plus-fill": 61813, + "bag-plus": 61814, + "bag-x-fill": 61815, + "bag-x": 61816, + "bag": 61817, + "bar-chart-fill": 61818, + "bar-chart-line-fill": 61819, + "bar-chart-line": 61820, + "bar-chart-steps": 61821, + "bar-chart": 61822, + "basket-fill": 61823, + "basket": 61824, + "basket2-fill": 61825, + "basket2": 61826, + "basket3-fill": 61827, + "basket3": 61828, + "battery-charging": 61829, + "battery-full": 61830, + "battery-half": 61831, + "battery": 61832, + "bell-fill": 61833, + "bell": 61834, + "bezier": 61835, + "bezier2": 61836, + "bicycle": 61837, + "binoculars-fill": 61838, + "binoculars": 61839, + "blockquote-left": 61840, + "blockquote-right": 61841, + "book-fill": 61842, + "book-half": 61843, + "book": 61844, + "bookmark-check-fill": 61845, + "bookmark-check": 61846, + "bookmark-dash-fill": 61847, + "bookmark-dash": 61848, + "bookmark-fill": 61849, + "bookmark-heart-fill": 61850, + "bookmark-heart": 61851, + "bookmark-plus-fill": 61852, + "bookmark-plus": 61853, + "bookmark-star-fill": 61854, + "bookmark-star": 61855, + "bookmark-x-fill": 61856, + "bookmark-x": 61857, + "bookmark": 61858, + "bookmarks-fill": 61859, + "bookmarks": 61860, + "bookshelf": 61861, + "bootstrap-fill": 61862, + "bootstrap-reboot": 61863, + "bootstrap": 61864, + "border-all": 61865, + "border-bottom": 61866, + "border-center": 61867, + "border-inner": 61868, + "border-left": 61869, + "border-middle": 61870, + "border-outer": 61871, + "border-right": 61872, + "border-style": 61873, + "border-top": 61874, + "border-width": 61875, + "border": 61876, + "bounding-box-circles": 61877, + "bounding-box": 61878, + "box-arrow-down-left": 61879, + "box-arrow-down-right": 61880, + "box-arrow-down": 61881, + "box-arrow-in-down-left": 61882, + "box-arrow-in-down-right": 61883, + "box-arrow-in-down": 61884, + "box-arrow-in-left": 61885, + "box-arrow-in-right": 61886, + "box-arrow-in-up-left": 61887, + "box-arrow-in-up-right": 61888, + "box-arrow-in-up": 61889, + "box-arrow-left": 61890, + "box-arrow-right": 61891, + "box-arrow-up-left": 61892, + "box-arrow-up-right": 61893, + "box-arrow-up": 61894, + "box-seam": 61895, + "box": 61896, + "braces": 61897, + "bricks": 61898, + "briefcase-fill": 61899, + "briefcase": 61900, + "brightness-alt-high-fill": 61901, + "brightness-alt-high": 61902, + "brightness-alt-low-fill": 61903, + "brightness-alt-low": 61904, + "brightness-high-fill": 61905, + "brightness-high": 61906, + "brightness-low-fill": 61907, + "brightness-low": 61908, + "broadcast-pin": 61909, + "broadcast": 61910, + "brush-fill": 61911, + "brush": 61912, + "bucket-fill": 61913, + "bucket": 61914, + "bug-fill": 61915, + "bug": 61916, + "building": 61917, + "bullseye": 61918, + "calculator-fill": 61919, + "calculator": 61920, + "calendar-check-fill": 61921, + "calendar-check": 61922, + "calendar-date-fill": 61923, + "calendar-date": 61924, + "calendar-day-fill": 61925, + "calendar-day": 61926, + "calendar-event-fill": 61927, + "calendar-event": 61928, + "calendar-fill": 61929, + "calendar-minus-fill": 61930, + "calendar-minus": 61931, + "calendar-month-fill": 61932, + "calendar-month": 61933, + "calendar-plus-fill": 61934, + "calendar-plus": 61935, + "calendar-range-fill": 61936, + "calendar-range": 61937, + "calendar-week-fill": 61938, + "calendar-week": 61939, + "calendar-x-fill": 61940, + "calendar-x": 61941, + "calendar": 61942, + "calendar2-check-fill": 61943, + "calendar2-check": 61944, + "calendar2-date-fill": 61945, + "calendar2-date": 61946, + "calendar2-day-fill": 61947, + "calendar2-day": 61948, + "calendar2-event-fill": 61949, + "calendar2-event": 61950, + "calendar2-fill": 61951, + "calendar2-minus-fill": 61952, + "calendar2-minus": 61953, + "calendar2-month-fill": 61954, + "calendar2-month": 61955, + "calendar2-plus-fill": 61956, + "calendar2-plus": 61957, + "calendar2-range-fill": 61958, + "calendar2-range": 61959, + "calendar2-week-fill": 61960, + "calendar2-week": 61961, + "calendar2-x-fill": 61962, + "calendar2-x": 61963, + "calendar2": 61964, + "calendar3-event-fill": 61965, + "calendar3-event": 61966, + "calendar3-fill": 61967, + "calendar3-range-fill": 61968, + "calendar3-range": 61969, + "calendar3-week-fill": 61970, + "calendar3-week": 61971, + "calendar3": 61972, + "calendar4-event": 61973, + "calendar4-range": 61974, + "calendar4-week": 61975, + "calendar4": 61976, + "camera-fill": 61977, + "camera-reels-fill": 61978, + "camera-reels": 61979, + "camera-video-fill": 61980, + "camera-video-off-fill": 61981, + "camera-video-off": 61982, + "camera-video": 61983, + "camera": 61984, + "camera2": 61985, + "capslock-fill": 61986, + "capslock": 61987, + "card-checklist": 61988, + "card-heading": 61989, + "card-image": 61990, + "card-list": 61991, + "card-text": 61992, + "caret-down-fill": 61993, + "caret-down-square-fill": 61994, + "caret-down-square": 61995, + "caret-down": 61996, + "caret-left-fill": 61997, + "caret-left-square-fill": 61998, + "caret-left-square": 61999, + "caret-left": 62000, + "caret-right-fill": 62001, + "caret-right-square-fill": 62002, + "caret-right-square": 62003, + "caret-right": 62004, + "caret-up-fill": 62005, + "caret-up-square-fill": 62006, + "caret-up-square": 62007, + "caret-up": 62008, + "cart-check-fill": 62009, + "cart-check": 62010, + "cart-dash-fill": 62011, + "cart-dash": 62012, + "cart-fill": 62013, + "cart-plus-fill": 62014, + "cart-plus": 62015, + "cart-x-fill": 62016, + "cart-x": 62017, + "cart": 62018, + "cart2": 62019, + "cart3": 62020, + "cart4": 62021, + "cash-stack": 62022, + "cash": 62023, + "cast": 62024, + "chat-dots-fill": 62025, + "chat-dots": 62026, + "chat-fill": 62027, + "chat-left-dots-fill": 62028, + "chat-left-dots": 62029, + "chat-left-fill": 62030, + "chat-left-quote-fill": 62031, + "chat-left-quote": 62032, + "chat-left-text-fill": 62033, + "chat-left-text": 62034, + "chat-left": 62035, + "chat-quote-fill": 62036, + "chat-quote": 62037, + "chat-right-dots-fill": 62038, + "chat-right-dots": 62039, + "chat-right-fill": 62040, + "chat-right-quote-fill": 62041, + "chat-right-quote": 62042, + "chat-right-text-fill": 62043, + "chat-right-text": 62044, + "chat-right": 62045, + "chat-square-dots-fill": 62046, + "chat-square-dots": 62047, + "chat-square-fill": 62048, + "chat-square-quote-fill": 62049, + "chat-square-quote": 62050, + "chat-square-text-fill": 62051, + "chat-square-text": 62052, + "chat-square": 62053, + "chat-text-fill": 62054, + "chat-text": 62055, + "chat": 62056, + "check-all": 62057, + "check-circle-fill": 62058, + "check-circle": 62059, + "check-square-fill": 62060, + "check-square": 62061, + "check": 62062, + "check2-all": 62063, + "check2-circle": 62064, + "check2-square": 62065, + "check2": 62066, + "chevron-bar-contract": 62067, + "chevron-bar-down": 62068, + "chevron-bar-expand": 62069, + "chevron-bar-left": 62070, + "chevron-bar-right": 62071, + "chevron-bar-up": 62072, + "chevron-compact-down": 62073, + "chevron-compact-left": 62074, + "chevron-compact-right": 62075, + "chevron-compact-up": 62076, + "chevron-contract": 62077, + "chevron-double-down": 62078, + "chevron-double-left": 62079, + "chevron-double-right": 62080, + "chevron-double-up": 62081, + "chevron-down": 62082, + "chevron-expand": 62083, + "chevron-left": 62084, + "chevron-right": 62085, + "chevron-up": 62086, + "circle-fill": 62087, + "circle-half": 62088, + "circle-square": 62089, + "circle": 62090, + "clipboard-check": 62091, + "clipboard-data": 62092, + "clipboard-minus": 62093, + "clipboard-plus": 62094, + "clipboard-x": 62095, + "clipboard": 62096, + "clock-fill": 62097, + "clock-history": 62098, + "clock": 62099, + "cloud-arrow-down-fill": 62100, + "cloud-arrow-down": 62101, + "cloud-arrow-up-fill": 62102, + "cloud-arrow-up": 62103, + "cloud-check-fill": 62104, + "cloud-check": 62105, + "cloud-download-fill": 62106, + "cloud-download": 62107, + "cloud-drizzle-fill": 62108, + "cloud-drizzle": 62109, + "cloud-fill": 62110, + "cloud-fog-fill": 62111, + "cloud-fog": 62112, + "cloud-fog2-fill": 62113, + "cloud-fog2": 62114, + "cloud-hail-fill": 62115, + "cloud-hail": 62116, + "cloud-haze-fill": 62118, + "cloud-haze": 62119, + "cloud-haze2-fill": 62120, + "cloud-lightning-fill": 62121, + "cloud-lightning-rain-fill": 62122, + "cloud-lightning-rain": 62123, + "cloud-lightning": 62124, + "cloud-minus-fill": 62125, + "cloud-minus": 62126, + "cloud-moon-fill": 62127, + "cloud-moon": 62128, + "cloud-plus-fill": 62129, + "cloud-plus": 62130, + "cloud-rain-fill": 62131, + "cloud-rain-heavy-fill": 62132, + "cloud-rain-heavy": 62133, + "cloud-rain": 62134, + "cloud-slash-fill": 62135, + "cloud-slash": 62136, + "cloud-sleet-fill": 62137, + "cloud-sleet": 62138, + "cloud-snow-fill": 62139, + "cloud-snow": 62140, + "cloud-sun-fill": 62141, + "cloud-sun": 62142, + "cloud-upload-fill": 62143, + "cloud-upload": 62144, + "cloud": 62145, + "clouds-fill": 62146, + "clouds": 62147, + "cloudy-fill": 62148, + "cloudy": 62149, + "code-slash": 62150, + "code-square": 62151, + "code": 62152, + "collection-fill": 62153, + "collection-play-fill": 62154, + "collection-play": 62155, + "collection": 62156, + "columns-gap": 62157, + "columns": 62158, + "command": 62159, + "compass-fill": 62160, + "compass": 62161, + "cone-striped": 62162, + "cone": 62163, + "controller": 62164, + "cpu-fill": 62165, + "cpu": 62166, + "credit-card-2-back-fill": 62167, + "credit-card-2-back": 62168, + "credit-card-2-front-fill": 62169, + "credit-card-2-front": 62170, + "credit-card-fill": 62171, + "credit-card": 62172, + "crop": 62173, + "cup-fill": 62174, + "cup-straw": 62175, + "cup": 62176, + "cursor-fill": 62177, + "cursor-text": 62178, + "cursor": 62179, + "dash-circle-dotted": 62180, + "dash-circle-fill": 62181, + "dash-circle": 62182, + "dash-square-dotted": 62183, + "dash-square-fill": 62184, + "dash-square": 62185, + "dash": 62186, + "diagram-2-fill": 62187, + "diagram-2": 62188, + "diagram-3-fill": 62189, + "diagram-3": 62190, + "diamond-fill": 62191, + "diamond-half": 62192, + "diamond": 62193, + "dice-1-fill": 62194, + "dice-1": 62195, + "dice-2-fill": 62196, + "dice-2": 62197, + "dice-3-fill": 62198, + "dice-3": 62199, + "dice-4-fill": 62200, + "dice-4": 62201, + "dice-5-fill": 62202, + "dice-5": 62203, + "dice-6-fill": 62204, + "dice-6": 62205, + "disc-fill": 62206, + "disc": 62207, + "discord": 62208, + "display-fill": 62209, + "display": 62210, + "distribute-horizontal": 62211, + "distribute-vertical": 62212, + "door-closed-fill": 62213, + "door-closed": 62214, + "door-open-fill": 62215, + "door-open": 62216, + "dot": 62217, + "download": 62218, + "droplet-fill": 62219, + "droplet-half": 62220, + "droplet": 62221, + "earbuds": 62222, + "easel-fill": 62223, + "easel": 62224, + "egg-fill": 62225, + "egg-fried": 62226, + "egg": 62227, + "eject-fill": 62228, + "eject": 62229, + "emoji-angry-fill": 62230, + "emoji-angry": 62231, + "emoji-dizzy-fill": 62232, + "emoji-dizzy": 62233, + "emoji-expressionless-fill": 62234, + "emoji-expressionless": 62235, + "emoji-frown-fill": 62236, + "emoji-frown": 62237, + "emoji-heart-eyes-fill": 62238, + "emoji-heart-eyes": 62239, + "emoji-laughing-fill": 62240, + "emoji-laughing": 62241, + "emoji-neutral-fill": 62242, + "emoji-neutral": 62243, + "emoji-smile-fill": 62244, + "emoji-smile-upside-down-fill": 62245, + "emoji-smile-upside-down": 62246, + "emoji-smile": 62247, + "emoji-sunglasses-fill": 62248, + "emoji-sunglasses": 62249, + "emoji-wink-fill": 62250, + "emoji-wink": 62251, + "envelope-fill": 62252, + "envelope-open-fill": 62253, + "envelope-open": 62254, + "envelope": 62255, + "eraser-fill": 62256, + "eraser": 62257, + "exclamation-circle-fill": 62258, + "exclamation-circle": 62259, + "exclamation-diamond-fill": 62260, + "exclamation-diamond": 62261, + "exclamation-octagon-fill": 62262, + "exclamation-octagon": 62263, + "exclamation-square-fill": 62264, + "exclamation-square": 62265, + "exclamation-triangle-fill": 62266, + "exclamation-triangle": 62267, + "exclamation": 62268, + "exclude": 62269, + "eye-fill": 62270, + "eye-slash-fill": 62271, + "eye-slash": 62272, + "eye": 62273, + "eyedropper": 62274, + "eyeglasses": 62275, + "facebook": 62276, + "file-arrow-down-fill": 62277, + "file-arrow-down": 62278, + "file-arrow-up-fill": 62279, + "file-arrow-up": 62280, + "file-bar-graph-fill": 62281, + "file-bar-graph": 62282, + "file-binary-fill": 62283, + "file-binary": 62284, + "file-break-fill": 62285, + "file-break": 62286, + "file-check-fill": 62287, + "file-check": 62288, + "file-code-fill": 62289, + "file-code": 62290, + "file-diff-fill": 62291, + "file-diff": 62292, + "file-earmark-arrow-down-fill": 62293, + "file-earmark-arrow-down": 62294, + "file-earmark-arrow-up-fill": 62295, + "file-earmark-arrow-up": 62296, + "file-earmark-bar-graph-fill": 62297, + "file-earmark-bar-graph": 62298, + "file-earmark-binary-fill": 62299, + "file-earmark-binary": 62300, + "file-earmark-break-fill": 62301, + "file-earmark-break": 62302, + "file-earmark-check-fill": 62303, + "file-earmark-check": 62304, + "file-earmark-code-fill": 62305, + "file-earmark-code": 62306, + "file-earmark-diff-fill": 62307, + "file-earmark-diff": 62308, + "file-earmark-easel-fill": 62309, + "file-earmark-easel": 62310, + "file-earmark-excel-fill": 62311, + "file-earmark-excel": 62312, + "file-earmark-fill": 62313, + "file-earmark-font-fill": 62314, + "file-earmark-font": 62315, + "file-earmark-image-fill": 62316, + "file-earmark-image": 62317, + "file-earmark-lock-fill": 62318, + "file-earmark-lock": 62319, + "file-earmark-lock2-fill": 62320, + "file-earmark-lock2": 62321, + "file-earmark-medical-fill": 62322, + "file-earmark-medical": 62323, + "file-earmark-minus-fill": 62324, + "file-earmark-minus": 62325, + "file-earmark-music-fill": 62326, + "file-earmark-music": 62327, + "file-earmark-person-fill": 62328, + "file-earmark-person": 62329, + "file-earmark-play-fill": 62330, + "file-earmark-play": 62331, + "file-earmark-plus-fill": 62332, + "file-earmark-plus": 62333, + "file-earmark-post-fill": 62334, + "file-earmark-post": 62335, + "file-earmark-ppt-fill": 62336, + "file-earmark-ppt": 62337, + "file-earmark-richtext-fill": 62338, + "file-earmark-richtext": 62339, + "file-earmark-ruled-fill": 62340, + "file-earmark-ruled": 62341, + "file-earmark-slides-fill": 62342, + "file-earmark-slides": 62343, + "file-earmark-spreadsheet-fill": 62344, + "file-earmark-spreadsheet": 62345, + "file-earmark-text-fill": 62346, + "file-earmark-text": 62347, + "file-earmark-word-fill": 62348, + "file-earmark-word": 62349, + "file-earmark-x-fill": 62350, + "file-earmark-x": 62351, + "file-earmark-zip-fill": 62352, + "file-earmark-zip": 62353, + "file-earmark": 62354, + "file-easel-fill": 62355, + "file-easel": 62356, + "file-excel-fill": 62357, + "file-excel": 62358, + "file-fill": 62359, + "file-font-fill": 62360, + "file-font": 62361, + "file-image-fill": 62362, + "file-image": 62363, + "file-lock-fill": 62364, + "file-lock": 62365, + "file-lock2-fill": 62366, + "file-lock2": 62367, + "file-medical-fill": 62368, + "file-medical": 62369, + "file-minus-fill": 62370, + "file-minus": 62371, + "file-music-fill": 62372, + "file-music": 62373, + "file-person-fill": 62374, + "file-person": 62375, + "file-play-fill": 62376, + "file-play": 62377, + "file-plus-fill": 62378, + "file-plus": 62379, + "file-post-fill": 62380, + "file-post": 62381, + "file-ppt-fill": 62382, + "file-ppt": 62383, + "file-richtext-fill": 62384, + "file-richtext": 62385, + "file-ruled-fill": 62386, + "file-ruled": 62387, + "file-slides-fill": 62388, + "file-slides": 62389, + "file-spreadsheet-fill": 62390, + "file-spreadsheet": 62391, + "file-text-fill": 62392, + "file-text": 62393, + "file-word-fill": 62394, + "file-word": 62395, + "file-x-fill": 62396, + "file-x": 62397, + "file-zip-fill": 62398, + "file-zip": 62399, + "file": 62400, + "files-alt": 62401, + "files": 62402, + "film": 62403, + "filter-circle-fill": 62404, + "filter-circle": 62405, + "filter-left": 62406, + "filter-right": 62407, + "filter-square-fill": 62408, + "filter-square": 62409, + "filter": 62410, + "flag-fill": 62411, + "flag": 62412, + "flower1": 62413, + "flower2": 62414, + "flower3": 62415, + "folder-check": 62416, + "folder-fill": 62417, + "folder-minus": 62418, + "folder-plus": 62419, + "folder-symlink-fill": 62420, + "folder-symlink": 62421, + "folder-x": 62422, + "folder": 62423, + "folder2-open": 62424, + "folder2": 62425, + "fonts": 62426, + "forward-fill": 62427, + "forward": 62428, + "front": 62429, + "fullscreen-exit": 62430, + "fullscreen": 62431, + "funnel-fill": 62432, + "funnel": 62433, + "gear-fill": 62434, + "gear-wide-connected": 62435, + "gear-wide": 62436, + "gear": 62437, + "gem": 62438, + "geo-alt-fill": 62439, + "geo-alt": 62440, + "geo-fill": 62441, + "geo": 62442, + "gift-fill": 62443, + "gift": 62444, + "github": 62445, + "globe": 62446, + "globe2": 62447, + "google": 62448, + "graph-down": 62449, + "graph-up": 62450, + "grid-1x2-fill": 62451, + "grid-1x2": 62452, + "grid-3x2-gap-fill": 62453, + "grid-3x2-gap": 62454, + "grid-3x2": 62455, + "grid-3x3-gap-fill": 62456, + "grid-3x3-gap": 62457, + "grid-3x3": 62458, + "grid-fill": 62459, + "grid": 62460, + "grip-horizontal": 62461, + "grip-vertical": 62462, + "hammer": 62463, + "hand-index-fill": 62464, + "hand-index-thumb-fill": 62465, + "hand-index-thumb": 62466, + "hand-index": 62467, + "hand-thumbs-down-fill": 62468, + "hand-thumbs-down": 62469, + "hand-thumbs-up-fill": 62470, + "hand-thumbs-up": 62471, + "handbag-fill": 62472, + "handbag": 62473, + "hash": 62474, + "hdd-fill": 62475, + "hdd-network-fill": 62476, + "hdd-network": 62477, + "hdd-rack-fill": 62478, + "hdd-rack": 62479, + "hdd-stack-fill": 62480, + "hdd-stack": 62481, + "hdd": 62482, + "headphones": 62483, + "headset": 62484, + "heart-fill": 62485, + "heart-half": 62486, + "heart": 62487, + "heptagon-fill": 62488, + "heptagon-half": 62489, + "heptagon": 62490, + "hexagon-fill": 62491, + "hexagon-half": 62492, + "hexagon": 62493, + "hourglass-bottom": 62494, + "hourglass-split": 62495, + "hourglass-top": 62496, + "hourglass": 62497, + "house-door-fill": 62498, + "house-door": 62499, + "house-fill": 62500, + "house": 62501, + "hr": 62502, + "hurricane": 62503, + "image-alt": 62504, + "image-fill": 62505, + "image": 62506, + "images": 62507, + "inbox-fill": 62508, + "inbox": 62509, + "inboxes-fill": 62510, + "inboxes": 62511, + "info-circle-fill": 62512, + "info-circle": 62513, + "info-square-fill": 62514, + "info-square": 62515, + "info": 62516, + "input-cursor-text": 62517, + "input-cursor": 62518, + "instagram": 62519, + "intersect": 62520, + "journal-album": 62521, + "journal-arrow-down": 62522, + "journal-arrow-up": 62523, + "journal-bookmark-fill": 62524, + "journal-bookmark": 62525, + "journal-check": 62526, + "journal-code": 62527, + "journal-medical": 62528, + "journal-minus": 62529, + "journal-plus": 62530, + "journal-richtext": 62531, + "journal-text": 62532, + "journal-x": 62533, + "journal": 62534, + "journals": 62535, + "joystick": 62536, + "justify-left": 62537, + "justify-right": 62538, + "justify": 62539, + "kanban-fill": 62540, + "kanban": 62541, + "key-fill": 62542, + "key": 62543, + "keyboard-fill": 62544, + "keyboard": 62545, + "ladder": 62546, + "lamp-fill": 62547, + "lamp": 62548, + "laptop-fill": 62549, + "laptop": 62550, + "layer-backward": 62551, + "layer-forward": 62552, + "layers-fill": 62553, + "layers-half": 62554, + "layers": 62555, + "layout-sidebar-inset-reverse": 62556, + "layout-sidebar-inset": 62557, + "layout-sidebar-reverse": 62558, + "layout-sidebar": 62559, + "layout-split": 62560, + "layout-text-sidebar-reverse": 62561, + "layout-text-sidebar": 62562, + "layout-text-window-reverse": 62563, + "layout-text-window": 62564, + "layout-three-columns": 62565, + "layout-wtf": 62566, + "life-preserver": 62567, + "lightbulb-fill": 62568, + "lightbulb-off-fill": 62569, + "lightbulb-off": 62570, + "lightbulb": 62571, + "lightning-charge-fill": 62572, + "lightning-charge": 62573, + "lightning-fill": 62574, + "lightning": 62575, + "link-45deg": 62576, + "link": 62577, + "linkedin": 62578, + "list-check": 62579, + "list-nested": 62580, + "list-ol": 62581, + "list-stars": 62582, + "list-task": 62583, + "list-ul": 62584, + "list": 62585, + "lock-fill": 62586, + "lock": 62587, + "mailbox": 62588, + "mailbox2": 62589, + "map-fill": 62590, + "map": 62591, + "markdown-fill": 62592, + "markdown": 62593, + "mask": 62594, + "megaphone-fill": 62595, + "megaphone": 62596, + "menu-app-fill": 62597, + "menu-app": 62598, + "menu-button-fill": 62599, + "menu-button-wide-fill": 62600, + "menu-button-wide": 62601, + "menu-button": 62602, + "menu-down": 62603, + "menu-up": 62604, + "mic-fill": 62605, + "mic-mute-fill": 62606, + "mic-mute": 62607, + "mic": 62608, + "minecart-loaded": 62609, + "minecart": 62610, + "moisture": 62611, + "moon-fill": 62612, + "moon-stars-fill": 62613, + "moon-stars": 62614, + "moon": 62615, + "mouse-fill": 62616, + "mouse": 62617, + "mouse2-fill": 62618, + "mouse2": 62619, + "mouse3-fill": 62620, + "mouse3": 62621, + "music-note-beamed": 62622, + "music-note-list": 62623, + "music-note": 62624, + "music-player-fill": 62625, + "music-player": 62626, + "newspaper": 62627, + "node-minus-fill": 62628, + "node-minus": 62629, + "node-plus-fill": 62630, + "node-plus": 62631, + "nut-fill": 62632, + "nut": 62633, + "octagon-fill": 62634, + "octagon-half": 62635, + "octagon": 62636, + "option": 62637, + "outlet": 62638, + "paint-bucket": 62639, + "palette-fill": 62640, + "palette": 62641, + "palette2": 62642, + "paperclip": 62643, + "paragraph": 62644, + "patch-check-fill": 62645, + "patch-check": 62646, + "patch-exclamation-fill": 62647, + "patch-exclamation": 62648, + "patch-minus-fill": 62649, + "patch-minus": 62650, + "patch-plus-fill": 62651, + "patch-plus": 62652, + "patch-question-fill": 62653, + "patch-question": 62654, + "pause-btn-fill": 62655, + "pause-btn": 62656, + "pause-circle-fill": 62657, + "pause-circle": 62658, + "pause-fill": 62659, + "pause": 62660, + "peace-fill": 62661, + "peace": 62662, + "pen-fill": 62663, + "pen": 62664, + "pencil-fill": 62665, + "pencil-square": 62666, + "pencil": 62667, + "pentagon-fill": 62668, + "pentagon-half": 62669, + "pentagon": 62670, + "people-fill": 62671, + "people": 62672, + "percent": 62673, + "person-badge-fill": 62674, + "person-badge": 62675, + "person-bounding-box": 62676, + "person-check-fill": 62677, + "person-check": 62678, + "person-circle": 62679, + "person-dash-fill": 62680, + "person-dash": 62681, + "person-fill": 62682, + "person-lines-fill": 62683, + "person-plus-fill": 62684, + "person-plus": 62685, + "person-square": 62686, + "person-x-fill": 62687, + "person-x": 62688, + "person": 62689, + "phone-fill": 62690, + "phone-landscape-fill": 62691, + "phone-landscape": 62692, + "phone-vibrate-fill": 62693, + "phone-vibrate": 62694, + "phone": 62695, + "pie-chart-fill": 62696, + "pie-chart": 62697, + "pin-angle-fill": 62698, + "pin-angle": 62699, + "pin-fill": 62700, + "pin": 62701, + "pip-fill": 62702, + "pip": 62703, + "play-btn-fill": 62704, + "play-btn": 62705, + "play-circle-fill": 62706, + "play-circle": 62707, + "play-fill": 62708, + "play": 62709, + "plug-fill": 62710, + "plug": 62711, + "plus-circle-dotted": 62712, + "plus-circle-fill": 62713, + "plus-circle": 62714, + "plus-square-dotted": 62715, + "plus-square-fill": 62716, + "plus-square": 62717, + "plus": 62718, + "power": 62719, + "printer-fill": 62720, + "printer": 62721, + "puzzle-fill": 62722, + "puzzle": 62723, + "question-circle-fill": 62724, + "question-circle": 62725, + "question-diamond-fill": 62726, + "question-diamond": 62727, + "question-octagon-fill": 62728, + "question-octagon": 62729, + "question-square-fill": 62730, + "question-square": 62731, + "question": 62732, + "rainbow": 62733, + "receipt-cutoff": 62734, + "receipt": 62735, + "reception-0": 62736, + "reception-1": 62737, + "reception-2": 62738, + "reception-3": 62739, + "reception-4": 62740, + "record-btn-fill": 62741, + "record-btn": 62742, + "record-circle-fill": 62743, + "record-circle": 62744, + "record-fill": 62745, + "record": 62746, + "record2-fill": 62747, + "record2": 62748, + "reply-all-fill": 62749, + "reply-all": 62750, + "reply-fill": 62751, + "reply": 62752, + "rss-fill": 62753, + "rss": 62754, + "rulers": 62755, + "save-fill": 62756, + "save": 62757, + "save2-fill": 62758, + "save2": 62759, + "scissors": 62760, + "screwdriver": 62761, + "search": 62762, + "segmented-nav": 62763, + "server": 62764, + "share-fill": 62765, + "share": 62766, + "shield-check": 62767, + "shield-exclamation": 62768, + "shield-fill-check": 62769, + "shield-fill-exclamation": 62770, + "shield-fill-minus": 62771, + "shield-fill-plus": 62772, + "shield-fill-x": 62773, + "shield-fill": 62774, + "shield-lock-fill": 62775, + "shield-lock": 62776, + "shield-minus": 62777, + "shield-plus": 62778, + "shield-shaded": 62779, + "shield-slash-fill": 62780, + "shield-slash": 62781, + "shield-x": 62782, + "shield": 62783, + "shift-fill": 62784, + "shift": 62785, + "shop-window": 62786, + "shop": 62787, + "shuffle": 62788, + "signpost-2-fill": 62789, + "signpost-2": 62790, + "signpost-fill": 62791, + "signpost-split-fill": 62792, + "signpost-split": 62793, + "signpost": 62794, + "sim-fill": 62795, + "sim": 62796, + "skip-backward-btn-fill": 62797, + "skip-backward-btn": 62798, + "skip-backward-circle-fill": 62799, + "skip-backward-circle": 62800, + "skip-backward-fill": 62801, + "skip-backward": 62802, + "skip-end-btn-fill": 62803, + "skip-end-btn": 62804, + "skip-end-circle-fill": 62805, + "skip-end-circle": 62806, + "skip-end-fill": 62807, + "skip-end": 62808, + "skip-forward-btn-fill": 62809, + "skip-forward-btn": 62810, + "skip-forward-circle-fill": 62811, + "skip-forward-circle": 62812, + "skip-forward-fill": 62813, + "skip-forward": 62814, + "skip-start-btn-fill": 62815, + "skip-start-btn": 62816, + "skip-start-circle-fill": 62817, + "skip-start-circle": 62818, + "skip-start-fill": 62819, + "skip-start": 62820, + "slack": 62821, + "slash-circle-fill": 62822, + "slash-circle": 62823, + "slash-square-fill": 62824, + "slash-square": 62825, + "slash": 62826, + "sliders": 62827, + "smartwatch": 62828, + "snow": 62829, + "snow2": 62830, + "snow3": 62831, + "sort-alpha-down-alt": 62832, + "sort-alpha-down": 62833, + "sort-alpha-up-alt": 62834, + "sort-alpha-up": 62835, + "sort-down-alt": 62836, + "sort-down": 62837, + "sort-numeric-down-alt": 62838, + "sort-numeric-down": 62839, + "sort-numeric-up-alt": 62840, + "sort-numeric-up": 62841, + "sort-up-alt": 62842, + "sort-up": 62843, + "soundwave": 62844, + "speaker-fill": 62845, + "speaker": 62846, + "speedometer": 62847, + "speedometer2": 62848, + "spellcheck": 62849, + "square-fill": 62850, + "square-half": 62851, + "square": 62852, + "stack": 62853, + "star-fill": 62854, + "star-half": 62855, + "star": 62856, + "stars": 62857, + "stickies-fill": 62858, + "stickies": 62859, + "sticky-fill": 62860, + "sticky": 62861, + "stop-btn-fill": 62862, + "stop-btn": 62863, + "stop-circle-fill": 62864, + "stop-circle": 62865, + "stop-fill": 62866, + "stop": 62867, + "stoplights-fill": 62868, + "stoplights": 62869, + "stopwatch-fill": 62870, + "stopwatch": 62871, + "subtract": 62872, + "suit-club-fill": 62873, + "suit-club": 62874, + "suit-diamond-fill": 62875, + "suit-diamond": 62876, + "suit-heart-fill": 62877, + "suit-heart": 62878, + "suit-spade-fill": 62879, + "suit-spade": 62880, + "sun-fill": 62881, + "sun": 62882, + "sunglasses": 62883, + "sunrise-fill": 62884, + "sunrise": 62885, + "sunset-fill": 62886, + "sunset": 62887, + "symmetry-horizontal": 62888, + "symmetry-vertical": 62889, + "table": 62890, + "tablet-fill": 62891, + "tablet-landscape-fill": 62892, + "tablet-landscape": 62893, + "tablet": 62894, + "tag-fill": 62895, + "tag": 62896, + "tags-fill": 62897, + "tags": 62898, + "telegram": 62899, + "telephone-fill": 62900, + "telephone-forward-fill": 62901, + "telephone-forward": 62902, + "telephone-inbound-fill": 62903, + "telephone-inbound": 62904, + "telephone-minus-fill": 62905, + "telephone-minus": 62906, + "telephone-outbound-fill": 62907, + "telephone-outbound": 62908, + "telephone-plus-fill": 62909, + "telephone-plus": 62910, + "telephone-x-fill": 62911, + "telephone-x": 62912, + "telephone": 62913, + "terminal-fill": 62914, + "terminal": 62915, + "text-center": 62916, + "text-indent-left": 62917, + "text-indent-right": 62918, + "text-left": 62919, + "text-paragraph": 62920, + "text-right": 62921, + "textarea-resize": 62922, + "textarea-t": 62923, + "textarea": 62924, + "thermometer-half": 62925, + "thermometer-high": 62926, + "thermometer-low": 62927, + "thermometer-snow": 62928, + "thermometer-sun": 62929, + "thermometer": 62930, + "three-dots-vertical": 62931, + "three-dots": 62932, + "toggle-off": 62933, + "toggle-on": 62934, + "toggle2-off": 62935, + "toggle2-on": 62936, + "toggles": 62937, + "toggles2": 62938, + "tools": 62939, + "tornado": 62940, + "trash-fill": 62941, + "trash": 62942, + "trash2-fill": 62943, + "trash2": 62944, + "tree-fill": 62945, + "tree": 62946, + "triangle-fill": 62947, + "triangle-half": 62948, + "triangle": 62949, + "trophy-fill": 62950, + "trophy": 62951, + "tropical-storm": 62952, + "truck-flatbed": 62953, + "truck": 62954, + "tsunami": 62955, + "tv-fill": 62956, + "tv": 62957, + "twitch": 62958, + "twitter": 62959, + "type-bold": 62960, + "type-h1": 62961, + "type-h2": 62962, + "type-h3": 62963, + "type-italic": 62964, + "type-strikethrough": 62965, + "type-underline": 62966, + "type": 62967, + "ui-checks-grid": 62968, + "ui-checks": 62969, + "ui-radios-grid": 62970, + "ui-radios": 62971, + "umbrella-fill": 62972, + "umbrella": 62973, + "union": 62974, + "unlock-fill": 62975, + "unlock": 62976, + "upc-scan": 62977, + "upc": 62978, + "upload": 62979, + "vector-pen": 62980, + "view-list": 62981, + "view-stacked": 62982, + "vinyl-fill": 62983, + "vinyl": 62984, + "voicemail": 62985, + "volume-down-fill": 62986, + "volume-down": 62987, + "volume-mute-fill": 62988, + "volume-mute": 62989, + "volume-off-fill": 62990, + "volume-off": 62991, + "volume-up-fill": 62992, + "volume-up": 62993, + "vr": 62994, + "wallet-fill": 62995, + "wallet": 62996, + "wallet2": 62997, + "watch": 62998, + "water": 62999, + "whatsapp": 63000, + "wifi-1": 63001, + "wifi-2": 63002, + "wifi-off": 63003, + "wifi": 63004, + "wind": 63005, + "window-dock": 63006, + "window-sidebar": 63007, + "window": 63008, + "wrench": 63009, + "x-circle-fill": 63010, + "x-circle": 63011, + "x-diamond-fill": 63012, + "x-diamond": 63013, + "x-octagon-fill": 63014, + "x-octagon": 63015, + "x-square-fill": 63016, + "x-square": 63017, + "x": 63018, + "youtube": 63019, + "zoom-in": 63020, + "zoom-out": 63021, + "bank": 63022, + "bank2": 63023, + "bell-slash-fill": 63024, + "bell-slash": 63025, + "cash-coin": 63026, + "check-lg": 63027, + "coin": 63028, + "currency-bitcoin": 63029, + "currency-dollar": 63030, + "currency-euro": 63031, + "currency-exchange": 63032, + "currency-pound": 63033, + "currency-yen": 63034, + "dash-lg": 63035, + "exclamation-lg": 63036, + "file-earmark-pdf-fill": 63037, + "file-earmark-pdf": 63038, + "file-pdf-fill": 63039, + "file-pdf": 63040, + "gender-ambiguous": 63041, + "gender-female": 63042, + "gender-male": 63043, + "gender-trans": 63044, + "headset-vr": 63045, + "info-lg": 63046, + "mastodon": 63047, + "messenger": 63048, + "piggy-bank-fill": 63049, + "piggy-bank": 63050, + "pin-map-fill": 63051, + "pin-map": 63052, + "plus-lg": 63053, + "question-lg": 63054, + "recycle": 63055, + "reddit": 63056, + "safe-fill": 63057, + "safe2-fill": 63058, + "safe2": 63059, + "sd-card-fill": 63060, + "sd-card": 63061, + "skype": 63062, + "slash-lg": 63063, + "translate": 63064, + "x-lg": 63065, + "safe": 63066, + "apple": 63067, + "microsoft": 63069, + "windows": 63070, + "behance": 63068, + "dribbble": 63071, + "line": 63072, + "medium": 63073, + "paypal": 63074, + "pinterest": 63075, + "signal": 63076, + "snapchat": 63077, + "spotify": 63078, + "stack-overflow": 63079, + "strava": 63080, + "wordpress": 63081, + "vimeo": 63082, + "activity": 63083, + "easel2-fill": 63084, + "easel2": 63085, + "easel3-fill": 63086, + "easel3": 63087, + "fan": 63088, + "fingerprint": 63089, + "graph-down-arrow": 63090, + "graph-up-arrow": 63091, + "hypnotize": 63092, + "magic": 63093, + "person-rolodex": 63094, + "person-video": 63095, + "person-video2": 63096, + "person-video3": 63097, + "person-workspace": 63098, + "radioactive": 63099, + "webcam-fill": 63100, + "webcam": 63101, + "yin-yang": 63102, + "bandaid-fill": 63104, + "bandaid": 63105, + "bluetooth": 63106, + "body-text": 63107, + "boombox": 63108, + "boxes": 63109, + "dpad-fill": 63110, + "dpad": 63111, + "ear-fill": 63112, + "ear": 63113, + "envelope-check-fill": 63115, + "envelope-check": 63116, + "envelope-dash-fill": 63118, + "envelope-dash": 63119, + "envelope-exclamation-fill": 63121, + "envelope-exclamation": 63122, + "envelope-plus-fill": 63123, + "envelope-plus": 63124, + "envelope-slash-fill": 63126, + "envelope-slash": 63127, + "envelope-x-fill": 63129, + "envelope-x": 63130, + "explicit-fill": 63131, + "explicit": 63132, + "git": 63133, + "infinity": 63134, + "list-columns-reverse": 63135, + "list-columns": 63136, + "meta": 63137, + "nintendo-switch": 63140, + "pc-display-horizontal": 63141, + "pc-display": 63142, + "pc-horizontal": 63143, + "pc": 63144, + "playstation": 63145, + "plus-slash-minus": 63146, + "projector-fill": 63147, + "projector": 63148, + "qr-code-scan": 63149, + "qr-code": 63150, + "quora": 63151, + "quote": 63152, + "robot": 63153, + "send-check-fill": 63154, + "send-check": 63155, + "send-dash-fill": 63156, + "send-dash": 63157, + "send-exclamation-fill": 63159, + "send-exclamation": 63160, + "send-fill": 63161, + "send-plus-fill": 63162, + "send-plus": 63163, + "send-slash-fill": 63164, + "send-slash": 63165, + "send-x-fill": 63166, + "send-x": 63167, + "send": 63168, + "steam": 63169, + "terminal-dash": 63171, + "terminal-plus": 63172, + "terminal-split": 63173, + "ticket-detailed-fill": 63174, + "ticket-detailed": 63175, + "ticket-fill": 63176, + "ticket-perforated-fill": 63177, + "ticket-perforated": 63178, + "ticket": 63179, + "tiktok": 63180, + "window-dash": 63181, + "window-desktop": 63182, + "window-fullscreen": 63183, + "window-plus": 63184, + "window-split": 63185, + "window-stack": 63186, + "window-x": 63187, + "xbox": 63188, + "ethernet": 63189, + "hdmi-fill": 63190, + "hdmi": 63191, + "usb-c-fill": 63192, + "usb-c": 63193, + "usb-fill": 63194, + "usb-plug-fill": 63195, + "usb-plug": 63196, + "usb-symbol": 63197, + "usb": 63198, + "boombox-fill": 63199, + "displayport": 63201, + "gpu-card": 63202, + "memory": 63203, + "modem-fill": 63204, + "modem": 63205, + "motherboard-fill": 63206, + "motherboard": 63207, + "optical-audio-fill": 63208, + "optical-audio": 63209, + "pci-card": 63210, + "router-fill": 63211, + "router": 63212, + "thunderbolt-fill": 63215, + "thunderbolt": 63216, + "usb-drive-fill": 63217, + "usb-drive": 63218, + "usb-micro-fill": 63219, + "usb-micro": 63220, + "usb-mini-fill": 63221, + "usb-mini": 63222, + "cloud-haze2": 63223, + "device-hdd-fill": 63224, + "device-hdd": 63225, + "device-ssd-fill": 63226, + "device-ssd": 63227, + "displayport-fill": 63228, + "mortarboard-fill": 63229, + "mortarboard": 63230, + "terminal-x": 63231, + "arrow-through-heart-fill": 63232, + "arrow-through-heart": 63233, + "badge-sd-fill": 63234, + "badge-sd": 63235, + "bag-heart-fill": 63236, + "bag-heart": 63237, + "balloon-fill": 63238, + "balloon-heart-fill": 63239, + "balloon-heart": 63240, + "balloon": 63241, + "box2-fill": 63242, + "box2-heart-fill": 63243, + "box2-heart": 63244, + "box2": 63245, + "braces-asterisk": 63246, + "calendar-heart-fill": 63247, + "calendar-heart": 63248, + "calendar2-heart-fill": 63249, + "calendar2-heart": 63250, + "chat-heart-fill": 63251, + "chat-heart": 63252, + "chat-left-heart-fill": 63253, + "chat-left-heart": 63254, + "chat-right-heart-fill": 63255, + "chat-right-heart": 63256, + "chat-square-heart-fill": 63257, + "chat-square-heart": 63258, + "clipboard-check-fill": 63259, + "clipboard-data-fill": 63260, + "clipboard-fill": 63261, + "clipboard-heart-fill": 63262, + "clipboard-heart": 63263, + "clipboard-minus-fill": 63264, + "clipboard-plus-fill": 63265, + "clipboard-pulse": 63266, + "clipboard-x-fill": 63267, + "clipboard2-check-fill": 63268, + "clipboard2-check": 63269, + "clipboard2-data-fill": 63270, + "clipboard2-data": 63271, + "clipboard2-fill": 63272, + "clipboard2-heart-fill": 63273, + "clipboard2-heart": 63274, + "clipboard2-minus-fill": 63275, + "clipboard2-minus": 63276, + "clipboard2-plus-fill": 63277, + "clipboard2-plus": 63278, + "clipboard2-pulse-fill": 63279, + "clipboard2-pulse": 63280, + "clipboard2-x-fill": 63281, + "clipboard2-x": 63282, + "clipboard2": 63283, + "emoji-kiss-fill": 63284, + "emoji-kiss": 63285, + "envelope-heart-fill": 63286, + "envelope-heart": 63287, + "envelope-open-heart-fill": 63288, + "envelope-open-heart": 63289, + "envelope-paper-fill": 63290, + "envelope-paper-heart-fill": 63291, + "envelope-paper-heart": 63292, + "envelope-paper": 63293, + "filetype-aac": 63294, + "filetype-ai": 63295, + "filetype-bmp": 63296, + "filetype-cs": 63297, + "filetype-css": 63298, + "filetype-csv": 63299, + "filetype-doc": 63300, + "filetype-docx": 63301, + "filetype-exe": 63302, + "filetype-gif": 63303, + "filetype-heic": 63304, + "filetype-html": 63305, + "filetype-java": 63306, + "filetype-jpg": 63307, + "filetype-js": 63308, + "filetype-jsx": 63309, + "filetype-key": 63310, + "filetype-m4p": 63311, + "filetype-md": 63312, + "filetype-mdx": 63313, + "filetype-mov": 63314, + "filetype-mp3": 63315, + "filetype-mp4": 63316, + "filetype-otf": 63317, + "filetype-pdf": 63318, + "filetype-php": 63319, + "filetype-png": 63320, + "filetype-ppt": 63322, + "filetype-psd": 63323, + "filetype-py": 63324, + "filetype-raw": 63325, + "filetype-rb": 63326, + "filetype-sass": 63327, + "filetype-scss": 63328, + "filetype-sh": 63329, + "filetype-svg": 63330, + "filetype-tiff": 63331, + "filetype-tsx": 63332, + "filetype-ttf": 63333, + "filetype-txt": 63334, + "filetype-wav": 63335, + "filetype-woff": 63336, + "filetype-xls": 63338, + "filetype-xml": 63339, + "filetype-yml": 63340, + "heart-arrow": 63341, + "heart-pulse-fill": 63342, + "heart-pulse": 63343, + "heartbreak-fill": 63344, + "heartbreak": 63345, + "hearts": 63346, + "hospital-fill": 63347, + "hospital": 63348, + "house-heart-fill": 63349, + "house-heart": 63350, + "incognito": 63351, + "magnet-fill": 63352, + "magnet": 63353, + "person-heart": 63354, + "person-hearts": 63355, + "phone-flip": 63356, + "plugin": 63357, + "postage-fill": 63358, + "postage-heart-fill": 63359, + "postage-heart": 63360, + "postage": 63361, + "postcard-fill": 63362, + "postcard-heart-fill": 63363, + "postcard-heart": 63364, + "postcard": 63365, + "search-heart-fill": 63366, + "search-heart": 63367, + "sliders2-vertical": 63368, + "sliders2": 63369, + "trash3-fill": 63370, + "trash3": 63371, + "valentine": 63372, + "valentine2": 63373, + "wrench-adjustable-circle-fill": 63374, + "wrench-adjustable-circle": 63375, + "wrench-adjustable": 63376, + "filetype-json": 63377, + "filetype-pptx": 63378, + "filetype-xlsx": 63379, + "1-circle-fill": 63382, + "1-circle": 63383, + "1-square-fill": 63384, + "1-square": 63385, + "2-circle-fill": 63388, + "2-circle": 63389, + "2-square-fill": 63390, + "2-square": 63391, + "3-circle-fill": 63394, + "3-circle": 63395, + "3-square-fill": 63396, + "3-square": 63397, + "4-circle-fill": 63400, + "4-circle": 63401, + "4-square-fill": 63402, + "4-square": 63403, + "5-circle-fill": 63406, + "5-circle": 63407, + "5-square-fill": 63408, + "5-square": 63409, + "6-circle-fill": 63412, + "6-circle": 63413, + "6-square-fill": 63414, + "6-square": 63415, + "7-circle-fill": 63418, + "7-circle": 63419, + "7-square-fill": 63420, + "7-square": 63421, + "8-circle-fill": 63424, + "8-circle": 63425, + "8-square-fill": 63426, + "8-square": 63427, + "9-circle-fill": 63430, + "9-circle": 63431, + "9-square-fill": 63432, + "9-square": 63433, + "airplane-engines-fill": 63434, + "airplane-engines": 63435, + "airplane-fill": 63436, + "airplane": 63437, + "alexa": 63438, + "alipay": 63439, + "android": 63440, + "android2": 63441, + "box-fill": 63442, + "box-seam-fill": 63443, + "browser-chrome": 63444, + "browser-edge": 63445, + "browser-firefox": 63446, + "browser-safari": 63447, + "c-circle-fill": 63450, + "c-circle": 63451, + "c-square-fill": 63452, + "c-square": 63453, + "capsule-pill": 63454, + "capsule": 63455, + "car-front-fill": 63456, + "car-front": 63457, + "cassette-fill": 63458, + "cassette": 63459, + "cc-circle-fill": 63462, + "cc-circle": 63463, + "cc-square-fill": 63464, + "cc-square": 63465, + "cup-hot-fill": 63466, + "cup-hot": 63467, + "currency-rupee": 63468, + "dropbox": 63469, + "escape": 63470, + "fast-forward-btn-fill": 63471, + "fast-forward-btn": 63472, + "fast-forward-circle-fill": 63473, + "fast-forward-circle": 63474, + "fast-forward-fill": 63475, + "fast-forward": 63476, + "filetype-sql": 63477, + "fire": 63478, + "google-play": 63479, + "h-circle-fill": 63482, + "h-circle": 63483, + "h-square-fill": 63484, + "h-square": 63485, + "indent": 63486, + "lungs-fill": 63487, + "lungs": 63488, + "microsoft-teams": 63489, + "p-circle-fill": 63492, + "p-circle": 63493, + "p-square-fill": 63494, + "p-square": 63495, + "pass-fill": 63496, + "pass": 63497, + "prescription": 63498, + "prescription2": 63499, + "r-circle-fill": 63502, + "r-circle": 63503, + "r-square-fill": 63504, + "r-square": 63505, + "repeat-1": 63506, + "repeat": 63507, + "rewind-btn-fill": 63508, + "rewind-btn": 63509, + "rewind-circle-fill": 63510, + "rewind-circle": 63511, + "rewind-fill": 63512, + "rewind": 63513, + "train-freight-front-fill": 63514, + "train-freight-front": 63515, + "train-front-fill": 63516, + "train-front": 63517, + "train-lightrail-front-fill": 63518, + "train-lightrail-front": 63519, + "truck-front-fill": 63520, + "truck-front": 63521, + "ubuntu": 63522, + "unindent": 63523, + "unity": 63524, + "universal-access-circle": 63525, + "universal-access": 63526, + "virus": 63527, + "virus2": 63528, + "wechat": 63529, + "yelp": 63530, + "sign-stop-fill": 63531, + "sign-stop-lights-fill": 63532, + "sign-stop-lights": 63533, + "sign-stop": 63534, + "sign-turn-left-fill": 63535, + "sign-turn-left": 63536, + "sign-turn-right-fill": 63537, + "sign-turn-right": 63538, + "sign-turn-slight-left-fill": 63539, + "sign-turn-slight-left": 63540, + "sign-turn-slight-right-fill": 63541, + "sign-turn-slight-right": 63542, + "sign-yield-fill": 63543, + "sign-yield": 63544, + "ev-station-fill": 63545, + "ev-station": 63546, + "fuel-pump-diesel-fill": 63547, + "fuel-pump-diesel": 63548, + "fuel-pump-fill": 63549, + "fuel-pump": 63550, + "0-circle-fill": 63551, + "0-circle": 63552, + "0-square-fill": 63553, + "0-square": 63554, + "rocket-fill": 63555, + "rocket-takeoff-fill": 63556, + "rocket-takeoff": 63557, + "rocket": 63558, + "stripe": 63559, + "subscript": 63560, + "superscript": 63561, + "trello": 63562, + "envelope-at-fill": 63563, + "envelope-at": 63564, + "regex": 63565, + "text-wrap": 63566, + "sign-dead-end-fill": 63567, + "sign-dead-end": 63568, + "sign-do-not-enter-fill": 63569, + "sign-do-not-enter": 63570, + "sign-intersection-fill": 63571, + "sign-intersection-side-fill": 63572, + "sign-intersection-side": 63573, + "sign-intersection-t-fill": 63574, + "sign-intersection-t": 63575, + "sign-intersection-y-fill": 63576, + "sign-intersection-y": 63577, + "sign-intersection": 63578, + "sign-merge-left-fill": 63579, + "sign-merge-left": 63580, + "sign-merge-right-fill": 63581, + "sign-merge-right": 63582, + "sign-no-left-turn-fill": 63583, + "sign-no-left-turn": 63584, + "sign-no-parking-fill": 63585, + "sign-no-parking": 63586, + "sign-no-right-turn-fill": 63587, + "sign-no-right-turn": 63588, + "sign-railroad-fill": 63589, + "sign-railroad": 63590, + "building-add": 63591, + "building-check": 63592, + "building-dash": 63593, + "building-down": 63594, + "building-exclamation": 63595, + "building-fill-add": 63596, + "building-fill-check": 63597, + "building-fill-dash": 63598, + "building-fill-down": 63599, + "building-fill-exclamation": 63600, + "building-fill-gear": 63601, + "building-fill-lock": 63602, + "building-fill-slash": 63603, + "building-fill-up": 63604, + "building-fill-x": 63605, + "building-fill": 63606, + "building-gear": 63607, + "building-lock": 63608, + "building-slash": 63609, + "building-up": 63610, + "building-x": 63611, + "buildings-fill": 63612, + "buildings": 63613, + "bus-front-fill": 63614, + "bus-front": 63615, + "ev-front-fill": 63616, + "ev-front": 63617, + "globe-americas": 63618, + "globe-asia-australia": 63619, + "globe-central-south-asia": 63620, + "globe-europe-africa": 63621, + "house-add-fill": 63622, + "house-add": 63623, + "house-check-fill": 63624, + "house-check": 63625, + "house-dash-fill": 63626, + "house-dash": 63627, + "house-down-fill": 63628, + "house-down": 63629, + "house-exclamation-fill": 63630, + "house-exclamation": 63631, + "house-gear-fill": 63632, + "house-gear": 63633, + "house-lock-fill": 63634, + "house-lock": 63635, + "house-slash-fill": 63636, + "house-slash": 63637, + "house-up-fill": 63638, + "house-up": 63639, + "house-x-fill": 63640, + "house-x": 63641, + "person-add": 63642, + "person-down": 63643, + "person-exclamation": 63644, + "person-fill-add": 63645, + "person-fill-check": 63646, + "person-fill-dash": 63647, + "person-fill-down": 63648, + "person-fill-exclamation": 63649, + "person-fill-gear": 63650, + "person-fill-lock": 63651, + "person-fill-slash": 63652, + "person-fill-up": 63653, + "person-fill-x": 63654, + "person-gear": 63655, + "person-lock": 63656, + "person-slash": 63657, + "person-up": 63658, + "scooter": 63659, + "taxi-front-fill": 63660, + "taxi-front": 63661, + "amd": 63662, + "database-add": 63663, + "database-check": 63664, + "database-dash": 63665, + "database-down": 63666, + "database-exclamation": 63667, + "database-fill-add": 63668, + "database-fill-check": 63669, + "database-fill-dash": 63670, + "database-fill-down": 63671, + "database-fill-exclamation": 63672, + "database-fill-gear": 63673, + "database-fill-lock": 63674, + "database-fill-slash": 63675, + "database-fill-up": 63676, + "database-fill-x": 63677, + "database-fill": 63678, + "database-gear": 63679, + "database-lock": 63680, + "database-slash": 63681, + "database-up": 63682, + "database-x": 63683, + "database": 63684, + "houses-fill": 63685, + "houses": 63686, + "nvidia": 63687, + "person-vcard-fill": 63688, + "person-vcard": 63689, + "sina-weibo": 63690, + "tencent-qq": 63691, + "wikipedia": 63692, + "alphabet-uppercase": 62117, + "alphabet": 63114, + "amazon": 63117, + "arrows-collapse-vertical": 63120, + "arrows-expand-vertical": 63125, + "arrows-vertical": 63128, + "arrows": 63138, + "ban-fill": 63139, + "ban": 63158, + "bing": 63170, + "cake": 63200, + "cake2": 63213, + "cookie": 63214, + "copy": 63321, + "crosshair": 63337, + "crosshair2": 63380, + "emoji-astonished-fill": 63381, + "emoji-astonished": 63386, + "emoji-grimace-fill": 63387, + "emoji-grimace": 63392, + "emoji-grin-fill": 63393, + "emoji-grin": 63398, + "emoji-surprise-fill": 63399, + "emoji-surprise": 63404, + "emoji-tear-fill": 63405, + "emoji-tear": 63410, + "envelope-arrow-down-fill": 63411, + "envelope-arrow-down": 63416, + "envelope-arrow-up-fill": 63417, + "envelope-arrow-up": 63422, + "feather": 63423, + "feather2": 63428, + "floppy-fill": 63429, + "floppy": 63448, + "floppy2-fill": 63449, + "floppy2": 63460, + "gitlab": 63461, + "highlighter": 63480, + "marker-tip": 63490, + "nvme-fill": 63491, + "nvme": 63500, + "opencollective": 63501, + "pci-card-network": 63693, + "pci-card-sound": 63694, + "radar": 63695, + "send-arrow-down-fill": 63696, + "send-arrow-down": 63697, + "send-arrow-up-fill": 63698, + "send-arrow-up": 63699, + "sim-slash-fill": 63700, + "sim-slash": 63701, + "sourceforge": 63702, + "substack": 63703, + "threads-fill": 63704, + "threads": 63705, + "transparency": 63706, + "twitter-x": 63707, + "type-h4": 63708, + "type-h5": 63709, + "type-h6": 63710, + "backpack-fill": 63711, + "backpack": 63712, + "backpack2-fill": 63713, + "backpack2": 63714, + "backpack3-fill": 63715, + "backpack3": 63716, + "backpack4-fill": 63717, + "backpack4": 63718, + "brilliance": 63719, + "cake-fill": 63720, + "cake2-fill": 63721, + "duffle-fill": 63722, + "duffle": 63723, + "exposure": 63724, + "gender-neuter": 63725, + "highlights": 63726, + "luggage-fill": 63727, + "luggage": 63728, + "mailbox-flag": 63729, + "mailbox2-flag": 63730, + "noise-reduction": 63731, + "passport-fill": 63732, + "passport": 63733, + "person-arms-up": 63734, + "person-raised-hand": 63735, + "person-standing-dress": 63736, + "person-standing": 63737, + "person-walking": 63738, + "person-wheelchair": 63739, + "shadows": 63740, + "suitcase-fill": 63741, + "suitcase-lg-fill": 63742, + "suitcase-lg": 63743, + "suitcase": 63744, + "suitcase2-fill": 63745, + "suitcase2": 63746, + "vignette": 63747, + "bluesky": 63481, + "tux": 63748, + "beaker-fill": 63749, + "beaker": 63750, + "flask-fill": 63751, + "flask-florence-fill": 63752, + "flask-florence": 63753, + "flask": 63754, + "leaf-fill": 63755, + "leaf": 63756, + "measuring-cup-fill": 63757, + "measuring-cup": 63758, + "unlock2-fill": 63759, + "unlock2": 63760, + "battery-low": 63761, + "anthropic": 63762, + "apple-music": 63763, + "claude": 63764, + "openai": 63765, + "perplexity": 63766, + "css": 63767, + "javascript": 63768, + "typescript": 63769, + "fork-knife": 63770, + "globe-americas-fill": 63771, + "globe-asia-australia-fill": 63772, + "globe-central-south-asia-fill": 63773, + "globe-europe-africa-fill": 63774 +} \ No newline at end of file diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.min.css b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.min.css new file mode 100644 index 00000000..706a5c8b --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */@font-face{font-display:block;font-family:bootstrap-icons;src:url("fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),url("fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff")}.bi::before,[class*=" bi-"]::before,[class^=bi-]::before{display:inline-block;font-family:bootstrap-icons!important;font-style:normal;font-weight:400!important;font-variant:normal;text-transform:none;line-height:1;vertical-align:-.125em;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.bi-123::before{content:"\f67f"}.bi-alarm-fill::before{content:"\f101"}.bi-alarm::before{content:"\f102"}.bi-align-bottom::before{content:"\f103"}.bi-align-center::before{content:"\f104"}.bi-align-end::before{content:"\f105"}.bi-align-middle::before{content:"\f106"}.bi-align-start::before{content:"\f107"}.bi-align-top::before{content:"\f108"}.bi-alt::before{content:"\f109"}.bi-app-indicator::before{content:"\f10a"}.bi-app::before{content:"\f10b"}.bi-archive-fill::before{content:"\f10c"}.bi-archive::before{content:"\f10d"}.bi-arrow-90deg-down::before{content:"\f10e"}.bi-arrow-90deg-left::before{content:"\f10f"}.bi-arrow-90deg-right::before{content:"\f110"}.bi-arrow-90deg-up::before{content:"\f111"}.bi-arrow-bar-down::before{content:"\f112"}.bi-arrow-bar-left::before{content:"\f113"}.bi-arrow-bar-right::before{content:"\f114"}.bi-arrow-bar-up::before{content:"\f115"}.bi-arrow-clockwise::before{content:"\f116"}.bi-arrow-counterclockwise::before{content:"\f117"}.bi-arrow-down-circle-fill::before{content:"\f118"}.bi-arrow-down-circle::before{content:"\f119"}.bi-arrow-down-left-circle-fill::before{content:"\f11a"}.bi-arrow-down-left-circle::before{content:"\f11b"}.bi-arrow-down-left-square-fill::before{content:"\f11c"}.bi-arrow-down-left-square::before{content:"\f11d"}.bi-arrow-down-left::before{content:"\f11e"}.bi-arrow-down-right-circle-fill::before{content:"\f11f"}.bi-arrow-down-right-circle::before{content:"\f120"}.bi-arrow-down-right-square-fill::before{content:"\f121"}.bi-arrow-down-right-square::before{content:"\f122"}.bi-arrow-down-right::before{content:"\f123"}.bi-arrow-down-short::before{content:"\f124"}.bi-arrow-down-square-fill::before{content:"\f125"}.bi-arrow-down-square::before{content:"\f126"}.bi-arrow-down-up::before{content:"\f127"}.bi-arrow-down::before{content:"\f128"}.bi-arrow-left-circle-fill::before{content:"\f129"}.bi-arrow-left-circle::before{content:"\f12a"}.bi-arrow-left-right::before{content:"\f12b"}.bi-arrow-left-short::before{content:"\f12c"}.bi-arrow-left-square-fill::before{content:"\f12d"}.bi-arrow-left-square::before{content:"\f12e"}.bi-arrow-left::before{content:"\f12f"}.bi-arrow-repeat::before{content:"\f130"}.bi-arrow-return-left::before{content:"\f131"}.bi-arrow-return-right::before{content:"\f132"}.bi-arrow-right-circle-fill::before{content:"\f133"}.bi-arrow-right-circle::before{content:"\f134"}.bi-arrow-right-short::before{content:"\f135"}.bi-arrow-right-square-fill::before{content:"\f136"}.bi-arrow-right-square::before{content:"\f137"}.bi-arrow-right::before{content:"\f138"}.bi-arrow-up-circle-fill::before{content:"\f139"}.bi-arrow-up-circle::before{content:"\f13a"}.bi-arrow-up-left-circle-fill::before{content:"\f13b"}.bi-arrow-up-left-circle::before{content:"\f13c"}.bi-arrow-up-left-square-fill::before{content:"\f13d"}.bi-arrow-up-left-square::before{content:"\f13e"}.bi-arrow-up-left::before{content:"\f13f"}.bi-arrow-up-right-circle-fill::before{content:"\f140"}.bi-arrow-up-right-circle::before{content:"\f141"}.bi-arrow-up-right-square-fill::before{content:"\f142"}.bi-arrow-up-right-square::before{content:"\f143"}.bi-arrow-up-right::before{content:"\f144"}.bi-arrow-up-short::before{content:"\f145"}.bi-arrow-up-square-fill::before{content:"\f146"}.bi-arrow-up-square::before{content:"\f147"}.bi-arrow-up::before{content:"\f148"}.bi-arrows-angle-contract::before{content:"\f149"}.bi-arrows-angle-expand::before{content:"\f14a"}.bi-arrows-collapse::before{content:"\f14b"}.bi-arrows-expand::before{content:"\f14c"}.bi-arrows-fullscreen::before{content:"\f14d"}.bi-arrows-move::before{content:"\f14e"}.bi-aspect-ratio-fill::before{content:"\f14f"}.bi-aspect-ratio::before{content:"\f150"}.bi-asterisk::before{content:"\f151"}.bi-at::before{content:"\f152"}.bi-award-fill::before{content:"\f153"}.bi-award::before{content:"\f154"}.bi-back::before{content:"\f155"}.bi-backspace-fill::before{content:"\f156"}.bi-backspace-reverse-fill::before{content:"\f157"}.bi-backspace-reverse::before{content:"\f158"}.bi-backspace::before{content:"\f159"}.bi-badge-3d-fill::before{content:"\f15a"}.bi-badge-3d::before{content:"\f15b"}.bi-badge-4k-fill::before{content:"\f15c"}.bi-badge-4k::before{content:"\f15d"}.bi-badge-8k-fill::before{content:"\f15e"}.bi-badge-8k::before{content:"\f15f"}.bi-badge-ad-fill::before{content:"\f160"}.bi-badge-ad::before{content:"\f161"}.bi-badge-ar-fill::before{content:"\f162"}.bi-badge-ar::before{content:"\f163"}.bi-badge-cc-fill::before{content:"\f164"}.bi-badge-cc::before{content:"\f165"}.bi-badge-hd-fill::before{content:"\f166"}.bi-badge-hd::before{content:"\f167"}.bi-badge-tm-fill::before{content:"\f168"}.bi-badge-tm::before{content:"\f169"}.bi-badge-vo-fill::before{content:"\f16a"}.bi-badge-vo::before{content:"\f16b"}.bi-badge-vr-fill::before{content:"\f16c"}.bi-badge-vr::before{content:"\f16d"}.bi-badge-wc-fill::before{content:"\f16e"}.bi-badge-wc::before{content:"\f16f"}.bi-bag-check-fill::before{content:"\f170"}.bi-bag-check::before{content:"\f171"}.bi-bag-dash-fill::before{content:"\f172"}.bi-bag-dash::before{content:"\f173"}.bi-bag-fill::before{content:"\f174"}.bi-bag-plus-fill::before{content:"\f175"}.bi-bag-plus::before{content:"\f176"}.bi-bag-x-fill::before{content:"\f177"}.bi-bag-x::before{content:"\f178"}.bi-bag::before{content:"\f179"}.bi-bar-chart-fill::before{content:"\f17a"}.bi-bar-chart-line-fill::before{content:"\f17b"}.bi-bar-chart-line::before{content:"\f17c"}.bi-bar-chart-steps::before{content:"\f17d"}.bi-bar-chart::before{content:"\f17e"}.bi-basket-fill::before{content:"\f17f"}.bi-basket::before{content:"\f180"}.bi-basket2-fill::before{content:"\f181"}.bi-basket2::before{content:"\f182"}.bi-basket3-fill::before{content:"\f183"}.bi-basket3::before{content:"\f184"}.bi-battery-charging::before{content:"\f185"}.bi-battery-full::before{content:"\f186"}.bi-battery-half::before{content:"\f187"}.bi-battery::before{content:"\f188"}.bi-bell-fill::before{content:"\f189"}.bi-bell::before{content:"\f18a"}.bi-bezier::before{content:"\f18b"}.bi-bezier2::before{content:"\f18c"}.bi-bicycle::before{content:"\f18d"}.bi-binoculars-fill::before{content:"\f18e"}.bi-binoculars::before{content:"\f18f"}.bi-blockquote-left::before{content:"\f190"}.bi-blockquote-right::before{content:"\f191"}.bi-book-fill::before{content:"\f192"}.bi-book-half::before{content:"\f193"}.bi-book::before{content:"\f194"}.bi-bookmark-check-fill::before{content:"\f195"}.bi-bookmark-check::before{content:"\f196"}.bi-bookmark-dash-fill::before{content:"\f197"}.bi-bookmark-dash::before{content:"\f198"}.bi-bookmark-fill::before{content:"\f199"}.bi-bookmark-heart-fill::before{content:"\f19a"}.bi-bookmark-heart::before{content:"\f19b"}.bi-bookmark-plus-fill::before{content:"\f19c"}.bi-bookmark-plus::before{content:"\f19d"}.bi-bookmark-star-fill::before{content:"\f19e"}.bi-bookmark-star::before{content:"\f19f"}.bi-bookmark-x-fill::before{content:"\f1a0"}.bi-bookmark-x::before{content:"\f1a1"}.bi-bookmark::before{content:"\f1a2"}.bi-bookmarks-fill::before{content:"\f1a3"}.bi-bookmarks::before{content:"\f1a4"}.bi-bookshelf::before{content:"\f1a5"}.bi-bootstrap-fill::before{content:"\f1a6"}.bi-bootstrap-reboot::before{content:"\f1a7"}.bi-bootstrap::before{content:"\f1a8"}.bi-border-all::before{content:"\f1a9"}.bi-border-bottom::before{content:"\f1aa"}.bi-border-center::before{content:"\f1ab"}.bi-border-inner::before{content:"\f1ac"}.bi-border-left::before{content:"\f1ad"}.bi-border-middle::before{content:"\f1ae"}.bi-border-outer::before{content:"\f1af"}.bi-border-right::before{content:"\f1b0"}.bi-border-style::before{content:"\f1b1"}.bi-border-top::before{content:"\f1b2"}.bi-border-width::before{content:"\f1b3"}.bi-border::before{content:"\f1b4"}.bi-bounding-box-circles::before{content:"\f1b5"}.bi-bounding-box::before{content:"\f1b6"}.bi-box-arrow-down-left::before{content:"\f1b7"}.bi-box-arrow-down-right::before{content:"\f1b8"}.bi-box-arrow-down::before{content:"\f1b9"}.bi-box-arrow-in-down-left::before{content:"\f1ba"}.bi-box-arrow-in-down-right::before{content:"\f1bb"}.bi-box-arrow-in-down::before{content:"\f1bc"}.bi-box-arrow-in-left::before{content:"\f1bd"}.bi-box-arrow-in-right::before{content:"\f1be"}.bi-box-arrow-in-up-left::before{content:"\f1bf"}.bi-box-arrow-in-up-right::before{content:"\f1c0"}.bi-box-arrow-in-up::before{content:"\f1c1"}.bi-box-arrow-left::before{content:"\f1c2"}.bi-box-arrow-right::before{content:"\f1c3"}.bi-box-arrow-up-left::before{content:"\f1c4"}.bi-box-arrow-up-right::before{content:"\f1c5"}.bi-box-arrow-up::before{content:"\f1c6"}.bi-box-seam::before{content:"\f1c7"}.bi-box::before{content:"\f1c8"}.bi-braces::before{content:"\f1c9"}.bi-bricks::before{content:"\f1ca"}.bi-briefcase-fill::before{content:"\f1cb"}.bi-briefcase::before{content:"\f1cc"}.bi-brightness-alt-high-fill::before{content:"\f1cd"}.bi-brightness-alt-high::before{content:"\f1ce"}.bi-brightness-alt-low-fill::before{content:"\f1cf"}.bi-brightness-alt-low::before{content:"\f1d0"}.bi-brightness-high-fill::before{content:"\f1d1"}.bi-brightness-high::before{content:"\f1d2"}.bi-brightness-low-fill::before{content:"\f1d3"}.bi-brightness-low::before{content:"\f1d4"}.bi-broadcast-pin::before{content:"\f1d5"}.bi-broadcast::before{content:"\f1d6"}.bi-brush-fill::before{content:"\f1d7"}.bi-brush::before{content:"\f1d8"}.bi-bucket-fill::before{content:"\f1d9"}.bi-bucket::before{content:"\f1da"}.bi-bug-fill::before{content:"\f1db"}.bi-bug::before{content:"\f1dc"}.bi-building::before{content:"\f1dd"}.bi-bullseye::before{content:"\f1de"}.bi-calculator-fill::before{content:"\f1df"}.bi-calculator::before{content:"\f1e0"}.bi-calendar-check-fill::before{content:"\f1e1"}.bi-calendar-check::before{content:"\f1e2"}.bi-calendar-date-fill::before{content:"\f1e3"}.bi-calendar-date::before{content:"\f1e4"}.bi-calendar-day-fill::before{content:"\f1e5"}.bi-calendar-day::before{content:"\f1e6"}.bi-calendar-event-fill::before{content:"\f1e7"}.bi-calendar-event::before{content:"\f1e8"}.bi-calendar-fill::before{content:"\f1e9"}.bi-calendar-minus-fill::before{content:"\f1ea"}.bi-calendar-minus::before{content:"\f1eb"}.bi-calendar-month-fill::before{content:"\f1ec"}.bi-calendar-month::before{content:"\f1ed"}.bi-calendar-plus-fill::before{content:"\f1ee"}.bi-calendar-plus::before{content:"\f1ef"}.bi-calendar-range-fill::before{content:"\f1f0"}.bi-calendar-range::before{content:"\f1f1"}.bi-calendar-week-fill::before{content:"\f1f2"}.bi-calendar-week::before{content:"\f1f3"}.bi-calendar-x-fill::before{content:"\f1f4"}.bi-calendar-x::before{content:"\f1f5"}.bi-calendar::before{content:"\f1f6"}.bi-calendar2-check-fill::before{content:"\f1f7"}.bi-calendar2-check::before{content:"\f1f8"}.bi-calendar2-date-fill::before{content:"\f1f9"}.bi-calendar2-date::before{content:"\f1fa"}.bi-calendar2-day-fill::before{content:"\f1fb"}.bi-calendar2-day::before{content:"\f1fc"}.bi-calendar2-event-fill::before{content:"\f1fd"}.bi-calendar2-event::before{content:"\f1fe"}.bi-calendar2-fill::before{content:"\f1ff"}.bi-calendar2-minus-fill::before{content:"\f200"}.bi-calendar2-minus::before{content:"\f201"}.bi-calendar2-month-fill::before{content:"\f202"}.bi-calendar2-month::before{content:"\f203"}.bi-calendar2-plus-fill::before{content:"\f204"}.bi-calendar2-plus::before{content:"\f205"}.bi-calendar2-range-fill::before{content:"\f206"}.bi-calendar2-range::before{content:"\f207"}.bi-calendar2-week-fill::before{content:"\f208"}.bi-calendar2-week::before{content:"\f209"}.bi-calendar2-x-fill::before{content:"\f20a"}.bi-calendar2-x::before{content:"\f20b"}.bi-calendar2::before{content:"\f20c"}.bi-calendar3-event-fill::before{content:"\f20d"}.bi-calendar3-event::before{content:"\f20e"}.bi-calendar3-fill::before{content:"\f20f"}.bi-calendar3-range-fill::before{content:"\f210"}.bi-calendar3-range::before{content:"\f211"}.bi-calendar3-week-fill::before{content:"\f212"}.bi-calendar3-week::before{content:"\f213"}.bi-calendar3::before{content:"\f214"}.bi-calendar4-event::before{content:"\f215"}.bi-calendar4-range::before{content:"\f216"}.bi-calendar4-week::before{content:"\f217"}.bi-calendar4::before{content:"\f218"}.bi-camera-fill::before{content:"\f219"}.bi-camera-reels-fill::before{content:"\f21a"}.bi-camera-reels::before{content:"\f21b"}.bi-camera-video-fill::before{content:"\f21c"}.bi-camera-video-off-fill::before{content:"\f21d"}.bi-camera-video-off::before{content:"\f21e"}.bi-camera-video::before{content:"\f21f"}.bi-camera::before{content:"\f220"}.bi-camera2::before{content:"\f221"}.bi-capslock-fill::before{content:"\f222"}.bi-capslock::before{content:"\f223"}.bi-card-checklist::before{content:"\f224"}.bi-card-heading::before{content:"\f225"}.bi-card-image::before{content:"\f226"}.bi-card-list::before{content:"\f227"}.bi-card-text::before{content:"\f228"}.bi-caret-down-fill::before{content:"\f229"}.bi-caret-down-square-fill::before{content:"\f22a"}.bi-caret-down-square::before{content:"\f22b"}.bi-caret-down::before{content:"\f22c"}.bi-caret-left-fill::before{content:"\f22d"}.bi-caret-left-square-fill::before{content:"\f22e"}.bi-caret-left-square::before{content:"\f22f"}.bi-caret-left::before{content:"\f230"}.bi-caret-right-fill::before{content:"\f231"}.bi-caret-right-square-fill::before{content:"\f232"}.bi-caret-right-square::before{content:"\f233"}.bi-caret-right::before{content:"\f234"}.bi-caret-up-fill::before{content:"\f235"}.bi-caret-up-square-fill::before{content:"\f236"}.bi-caret-up-square::before{content:"\f237"}.bi-caret-up::before{content:"\f238"}.bi-cart-check-fill::before{content:"\f239"}.bi-cart-check::before{content:"\f23a"}.bi-cart-dash-fill::before{content:"\f23b"}.bi-cart-dash::before{content:"\f23c"}.bi-cart-fill::before{content:"\f23d"}.bi-cart-plus-fill::before{content:"\f23e"}.bi-cart-plus::before{content:"\f23f"}.bi-cart-x-fill::before{content:"\f240"}.bi-cart-x::before{content:"\f241"}.bi-cart::before{content:"\f242"}.bi-cart2::before{content:"\f243"}.bi-cart3::before{content:"\f244"}.bi-cart4::before{content:"\f245"}.bi-cash-stack::before{content:"\f246"}.bi-cash::before{content:"\f247"}.bi-cast::before{content:"\f248"}.bi-chat-dots-fill::before{content:"\f249"}.bi-chat-dots::before{content:"\f24a"}.bi-chat-fill::before{content:"\f24b"}.bi-chat-left-dots-fill::before{content:"\f24c"}.bi-chat-left-dots::before{content:"\f24d"}.bi-chat-left-fill::before{content:"\f24e"}.bi-chat-left-quote-fill::before{content:"\f24f"}.bi-chat-left-quote::before{content:"\f250"}.bi-chat-left-text-fill::before{content:"\f251"}.bi-chat-left-text::before{content:"\f252"}.bi-chat-left::before{content:"\f253"}.bi-chat-quote-fill::before{content:"\f254"}.bi-chat-quote::before{content:"\f255"}.bi-chat-right-dots-fill::before{content:"\f256"}.bi-chat-right-dots::before{content:"\f257"}.bi-chat-right-fill::before{content:"\f258"}.bi-chat-right-quote-fill::before{content:"\f259"}.bi-chat-right-quote::before{content:"\f25a"}.bi-chat-right-text-fill::before{content:"\f25b"}.bi-chat-right-text::before{content:"\f25c"}.bi-chat-right::before{content:"\f25d"}.bi-chat-square-dots-fill::before{content:"\f25e"}.bi-chat-square-dots::before{content:"\f25f"}.bi-chat-square-fill::before{content:"\f260"}.bi-chat-square-quote-fill::before{content:"\f261"}.bi-chat-square-quote::before{content:"\f262"}.bi-chat-square-text-fill::before{content:"\f263"}.bi-chat-square-text::before{content:"\f264"}.bi-chat-square::before{content:"\f265"}.bi-chat-text-fill::before{content:"\f266"}.bi-chat-text::before{content:"\f267"}.bi-chat::before{content:"\f268"}.bi-check-all::before{content:"\f269"}.bi-check-circle-fill::before{content:"\f26a"}.bi-check-circle::before{content:"\f26b"}.bi-check-square-fill::before{content:"\f26c"}.bi-check-square::before{content:"\f26d"}.bi-check::before{content:"\f26e"}.bi-check2-all::before{content:"\f26f"}.bi-check2-circle::before{content:"\f270"}.bi-check2-square::before{content:"\f271"}.bi-check2::before{content:"\f272"}.bi-chevron-bar-contract::before{content:"\f273"}.bi-chevron-bar-down::before{content:"\f274"}.bi-chevron-bar-expand::before{content:"\f275"}.bi-chevron-bar-left::before{content:"\f276"}.bi-chevron-bar-right::before{content:"\f277"}.bi-chevron-bar-up::before{content:"\f278"}.bi-chevron-compact-down::before{content:"\f279"}.bi-chevron-compact-left::before{content:"\f27a"}.bi-chevron-compact-right::before{content:"\f27b"}.bi-chevron-compact-up::before{content:"\f27c"}.bi-chevron-contract::before{content:"\f27d"}.bi-chevron-double-down::before{content:"\f27e"}.bi-chevron-double-left::before{content:"\f27f"}.bi-chevron-double-right::before{content:"\f280"}.bi-chevron-double-up::before{content:"\f281"}.bi-chevron-down::before{content:"\f282"}.bi-chevron-expand::before{content:"\f283"}.bi-chevron-left::before{content:"\f284"}.bi-chevron-right::before{content:"\f285"}.bi-chevron-up::before{content:"\f286"}.bi-circle-fill::before{content:"\f287"}.bi-circle-half::before{content:"\f288"}.bi-circle-square::before{content:"\f289"}.bi-circle::before{content:"\f28a"}.bi-clipboard-check::before{content:"\f28b"}.bi-clipboard-data::before{content:"\f28c"}.bi-clipboard-minus::before{content:"\f28d"}.bi-clipboard-plus::before{content:"\f28e"}.bi-clipboard-x::before{content:"\f28f"}.bi-clipboard::before{content:"\f290"}.bi-clock-fill::before{content:"\f291"}.bi-clock-history::before{content:"\f292"}.bi-clock::before{content:"\f293"}.bi-cloud-arrow-down-fill::before{content:"\f294"}.bi-cloud-arrow-down::before{content:"\f295"}.bi-cloud-arrow-up-fill::before{content:"\f296"}.bi-cloud-arrow-up::before{content:"\f297"}.bi-cloud-check-fill::before{content:"\f298"}.bi-cloud-check::before{content:"\f299"}.bi-cloud-download-fill::before{content:"\f29a"}.bi-cloud-download::before{content:"\f29b"}.bi-cloud-drizzle-fill::before{content:"\f29c"}.bi-cloud-drizzle::before{content:"\f29d"}.bi-cloud-fill::before{content:"\f29e"}.bi-cloud-fog-fill::before{content:"\f29f"}.bi-cloud-fog::before{content:"\f2a0"}.bi-cloud-fog2-fill::before{content:"\f2a1"}.bi-cloud-fog2::before{content:"\f2a2"}.bi-cloud-hail-fill::before{content:"\f2a3"}.bi-cloud-hail::before{content:"\f2a4"}.bi-cloud-haze-fill::before{content:"\f2a6"}.bi-cloud-haze::before{content:"\f2a7"}.bi-cloud-haze2-fill::before{content:"\f2a8"}.bi-cloud-lightning-fill::before{content:"\f2a9"}.bi-cloud-lightning-rain-fill::before{content:"\f2aa"}.bi-cloud-lightning-rain::before{content:"\f2ab"}.bi-cloud-lightning::before{content:"\f2ac"}.bi-cloud-minus-fill::before{content:"\f2ad"}.bi-cloud-minus::before{content:"\f2ae"}.bi-cloud-moon-fill::before{content:"\f2af"}.bi-cloud-moon::before{content:"\f2b0"}.bi-cloud-plus-fill::before{content:"\f2b1"}.bi-cloud-plus::before{content:"\f2b2"}.bi-cloud-rain-fill::before{content:"\f2b3"}.bi-cloud-rain-heavy-fill::before{content:"\f2b4"}.bi-cloud-rain-heavy::before{content:"\f2b5"}.bi-cloud-rain::before{content:"\f2b6"}.bi-cloud-slash-fill::before{content:"\f2b7"}.bi-cloud-slash::before{content:"\f2b8"}.bi-cloud-sleet-fill::before{content:"\f2b9"}.bi-cloud-sleet::before{content:"\f2ba"}.bi-cloud-snow-fill::before{content:"\f2bb"}.bi-cloud-snow::before{content:"\f2bc"}.bi-cloud-sun-fill::before{content:"\f2bd"}.bi-cloud-sun::before{content:"\f2be"}.bi-cloud-upload-fill::before{content:"\f2bf"}.bi-cloud-upload::before{content:"\f2c0"}.bi-cloud::before{content:"\f2c1"}.bi-clouds-fill::before{content:"\f2c2"}.bi-clouds::before{content:"\f2c3"}.bi-cloudy-fill::before{content:"\f2c4"}.bi-cloudy::before{content:"\f2c5"}.bi-code-slash::before{content:"\f2c6"}.bi-code-square::before{content:"\f2c7"}.bi-code::before{content:"\f2c8"}.bi-collection-fill::before{content:"\f2c9"}.bi-collection-play-fill::before{content:"\f2ca"}.bi-collection-play::before{content:"\f2cb"}.bi-collection::before{content:"\f2cc"}.bi-columns-gap::before{content:"\f2cd"}.bi-columns::before{content:"\f2ce"}.bi-command::before{content:"\f2cf"}.bi-compass-fill::before{content:"\f2d0"}.bi-compass::before{content:"\f2d1"}.bi-cone-striped::before{content:"\f2d2"}.bi-cone::before{content:"\f2d3"}.bi-controller::before{content:"\f2d4"}.bi-cpu-fill::before{content:"\f2d5"}.bi-cpu::before{content:"\f2d6"}.bi-credit-card-2-back-fill::before{content:"\f2d7"}.bi-credit-card-2-back::before{content:"\f2d8"}.bi-credit-card-2-front-fill::before{content:"\f2d9"}.bi-credit-card-2-front::before{content:"\f2da"}.bi-credit-card-fill::before{content:"\f2db"}.bi-credit-card::before{content:"\f2dc"}.bi-crop::before{content:"\f2dd"}.bi-cup-fill::before{content:"\f2de"}.bi-cup-straw::before{content:"\f2df"}.bi-cup::before{content:"\f2e0"}.bi-cursor-fill::before{content:"\f2e1"}.bi-cursor-text::before{content:"\f2e2"}.bi-cursor::before{content:"\f2e3"}.bi-dash-circle-dotted::before{content:"\f2e4"}.bi-dash-circle-fill::before{content:"\f2e5"}.bi-dash-circle::before{content:"\f2e6"}.bi-dash-square-dotted::before{content:"\f2e7"}.bi-dash-square-fill::before{content:"\f2e8"}.bi-dash-square::before{content:"\f2e9"}.bi-dash::before{content:"\f2ea"}.bi-diagram-2-fill::before{content:"\f2eb"}.bi-diagram-2::before{content:"\f2ec"}.bi-diagram-3-fill::before{content:"\f2ed"}.bi-diagram-3::before{content:"\f2ee"}.bi-diamond-fill::before{content:"\f2ef"}.bi-diamond-half::before{content:"\f2f0"}.bi-diamond::before{content:"\f2f1"}.bi-dice-1-fill::before{content:"\f2f2"}.bi-dice-1::before{content:"\f2f3"}.bi-dice-2-fill::before{content:"\f2f4"}.bi-dice-2::before{content:"\f2f5"}.bi-dice-3-fill::before{content:"\f2f6"}.bi-dice-3::before{content:"\f2f7"}.bi-dice-4-fill::before{content:"\f2f8"}.bi-dice-4::before{content:"\f2f9"}.bi-dice-5-fill::before{content:"\f2fa"}.bi-dice-5::before{content:"\f2fb"}.bi-dice-6-fill::before{content:"\f2fc"}.bi-dice-6::before{content:"\f2fd"}.bi-disc-fill::before{content:"\f2fe"}.bi-disc::before{content:"\f2ff"}.bi-discord::before{content:"\f300"}.bi-display-fill::before{content:"\f301"}.bi-display::before{content:"\f302"}.bi-distribute-horizontal::before{content:"\f303"}.bi-distribute-vertical::before{content:"\f304"}.bi-door-closed-fill::before{content:"\f305"}.bi-door-closed::before{content:"\f306"}.bi-door-open-fill::before{content:"\f307"}.bi-door-open::before{content:"\f308"}.bi-dot::before{content:"\f309"}.bi-download::before{content:"\f30a"}.bi-droplet-fill::before{content:"\f30b"}.bi-droplet-half::before{content:"\f30c"}.bi-droplet::before{content:"\f30d"}.bi-earbuds::before{content:"\f30e"}.bi-easel-fill::before{content:"\f30f"}.bi-easel::before{content:"\f310"}.bi-egg-fill::before{content:"\f311"}.bi-egg-fried::before{content:"\f312"}.bi-egg::before{content:"\f313"}.bi-eject-fill::before{content:"\f314"}.bi-eject::before{content:"\f315"}.bi-emoji-angry-fill::before{content:"\f316"}.bi-emoji-angry::before{content:"\f317"}.bi-emoji-dizzy-fill::before{content:"\f318"}.bi-emoji-dizzy::before{content:"\f319"}.bi-emoji-expressionless-fill::before{content:"\f31a"}.bi-emoji-expressionless::before{content:"\f31b"}.bi-emoji-frown-fill::before{content:"\f31c"}.bi-emoji-frown::before{content:"\f31d"}.bi-emoji-heart-eyes-fill::before{content:"\f31e"}.bi-emoji-heart-eyes::before{content:"\f31f"}.bi-emoji-laughing-fill::before{content:"\f320"}.bi-emoji-laughing::before{content:"\f321"}.bi-emoji-neutral-fill::before{content:"\f322"}.bi-emoji-neutral::before{content:"\f323"}.bi-emoji-smile-fill::before{content:"\f324"}.bi-emoji-smile-upside-down-fill::before{content:"\f325"}.bi-emoji-smile-upside-down::before{content:"\f326"}.bi-emoji-smile::before{content:"\f327"}.bi-emoji-sunglasses-fill::before{content:"\f328"}.bi-emoji-sunglasses::before{content:"\f329"}.bi-emoji-wink-fill::before{content:"\f32a"}.bi-emoji-wink::before{content:"\f32b"}.bi-envelope-fill::before{content:"\f32c"}.bi-envelope-open-fill::before{content:"\f32d"}.bi-envelope-open::before{content:"\f32e"}.bi-envelope::before{content:"\f32f"}.bi-eraser-fill::before{content:"\f330"}.bi-eraser::before{content:"\f331"}.bi-exclamation-circle-fill::before{content:"\f332"}.bi-exclamation-circle::before{content:"\f333"}.bi-exclamation-diamond-fill::before{content:"\f334"}.bi-exclamation-diamond::before{content:"\f335"}.bi-exclamation-octagon-fill::before{content:"\f336"}.bi-exclamation-octagon::before{content:"\f337"}.bi-exclamation-square-fill::before{content:"\f338"}.bi-exclamation-square::before{content:"\f339"}.bi-exclamation-triangle-fill::before{content:"\f33a"}.bi-exclamation-triangle::before{content:"\f33b"}.bi-exclamation::before{content:"\f33c"}.bi-exclude::before{content:"\f33d"}.bi-eye-fill::before{content:"\f33e"}.bi-eye-slash-fill::before{content:"\f33f"}.bi-eye-slash::before{content:"\f340"}.bi-eye::before{content:"\f341"}.bi-eyedropper::before{content:"\f342"}.bi-eyeglasses::before{content:"\f343"}.bi-facebook::before{content:"\f344"}.bi-file-arrow-down-fill::before{content:"\f345"}.bi-file-arrow-down::before{content:"\f346"}.bi-file-arrow-up-fill::before{content:"\f347"}.bi-file-arrow-up::before{content:"\f348"}.bi-file-bar-graph-fill::before{content:"\f349"}.bi-file-bar-graph::before{content:"\f34a"}.bi-file-binary-fill::before{content:"\f34b"}.bi-file-binary::before{content:"\f34c"}.bi-file-break-fill::before{content:"\f34d"}.bi-file-break::before{content:"\f34e"}.bi-file-check-fill::before{content:"\f34f"}.bi-file-check::before{content:"\f350"}.bi-file-code-fill::before{content:"\f351"}.bi-file-code::before{content:"\f352"}.bi-file-diff-fill::before{content:"\f353"}.bi-file-diff::before{content:"\f354"}.bi-file-earmark-arrow-down-fill::before{content:"\f355"}.bi-file-earmark-arrow-down::before{content:"\f356"}.bi-file-earmark-arrow-up-fill::before{content:"\f357"}.bi-file-earmark-arrow-up::before{content:"\f358"}.bi-file-earmark-bar-graph-fill::before{content:"\f359"}.bi-file-earmark-bar-graph::before{content:"\f35a"}.bi-file-earmark-binary-fill::before{content:"\f35b"}.bi-file-earmark-binary::before{content:"\f35c"}.bi-file-earmark-break-fill::before{content:"\f35d"}.bi-file-earmark-break::before{content:"\f35e"}.bi-file-earmark-check-fill::before{content:"\f35f"}.bi-file-earmark-check::before{content:"\f360"}.bi-file-earmark-code-fill::before{content:"\f361"}.bi-file-earmark-code::before{content:"\f362"}.bi-file-earmark-diff-fill::before{content:"\f363"}.bi-file-earmark-diff::before{content:"\f364"}.bi-file-earmark-easel-fill::before{content:"\f365"}.bi-file-earmark-easel::before{content:"\f366"}.bi-file-earmark-excel-fill::before{content:"\f367"}.bi-file-earmark-excel::before{content:"\f368"}.bi-file-earmark-fill::before{content:"\f369"}.bi-file-earmark-font-fill::before{content:"\f36a"}.bi-file-earmark-font::before{content:"\f36b"}.bi-file-earmark-image-fill::before{content:"\f36c"}.bi-file-earmark-image::before{content:"\f36d"}.bi-file-earmark-lock-fill::before{content:"\f36e"}.bi-file-earmark-lock::before{content:"\f36f"}.bi-file-earmark-lock2-fill::before{content:"\f370"}.bi-file-earmark-lock2::before{content:"\f371"}.bi-file-earmark-medical-fill::before{content:"\f372"}.bi-file-earmark-medical::before{content:"\f373"}.bi-file-earmark-minus-fill::before{content:"\f374"}.bi-file-earmark-minus::before{content:"\f375"}.bi-file-earmark-music-fill::before{content:"\f376"}.bi-file-earmark-music::before{content:"\f377"}.bi-file-earmark-person-fill::before{content:"\f378"}.bi-file-earmark-person::before{content:"\f379"}.bi-file-earmark-play-fill::before{content:"\f37a"}.bi-file-earmark-play::before{content:"\f37b"}.bi-file-earmark-plus-fill::before{content:"\f37c"}.bi-file-earmark-plus::before{content:"\f37d"}.bi-file-earmark-post-fill::before{content:"\f37e"}.bi-file-earmark-post::before{content:"\f37f"}.bi-file-earmark-ppt-fill::before{content:"\f380"}.bi-file-earmark-ppt::before{content:"\f381"}.bi-file-earmark-richtext-fill::before{content:"\f382"}.bi-file-earmark-richtext::before{content:"\f383"}.bi-file-earmark-ruled-fill::before{content:"\f384"}.bi-file-earmark-ruled::before{content:"\f385"}.bi-file-earmark-slides-fill::before{content:"\f386"}.bi-file-earmark-slides::before{content:"\f387"}.bi-file-earmark-spreadsheet-fill::before{content:"\f388"}.bi-file-earmark-spreadsheet::before{content:"\f389"}.bi-file-earmark-text-fill::before{content:"\f38a"}.bi-file-earmark-text::before{content:"\f38b"}.bi-file-earmark-word-fill::before{content:"\f38c"}.bi-file-earmark-word::before{content:"\f38d"}.bi-file-earmark-x-fill::before{content:"\f38e"}.bi-file-earmark-x::before{content:"\f38f"}.bi-file-earmark-zip-fill::before{content:"\f390"}.bi-file-earmark-zip::before{content:"\f391"}.bi-file-earmark::before{content:"\f392"}.bi-file-easel-fill::before{content:"\f393"}.bi-file-easel::before{content:"\f394"}.bi-file-excel-fill::before{content:"\f395"}.bi-file-excel::before{content:"\f396"}.bi-file-fill::before{content:"\f397"}.bi-file-font-fill::before{content:"\f398"}.bi-file-font::before{content:"\f399"}.bi-file-image-fill::before{content:"\f39a"}.bi-file-image::before{content:"\f39b"}.bi-file-lock-fill::before{content:"\f39c"}.bi-file-lock::before{content:"\f39d"}.bi-file-lock2-fill::before{content:"\f39e"}.bi-file-lock2::before{content:"\f39f"}.bi-file-medical-fill::before{content:"\f3a0"}.bi-file-medical::before{content:"\f3a1"}.bi-file-minus-fill::before{content:"\f3a2"}.bi-file-minus::before{content:"\f3a3"}.bi-file-music-fill::before{content:"\f3a4"}.bi-file-music::before{content:"\f3a5"}.bi-file-person-fill::before{content:"\f3a6"}.bi-file-person::before{content:"\f3a7"}.bi-file-play-fill::before{content:"\f3a8"}.bi-file-play::before{content:"\f3a9"}.bi-file-plus-fill::before{content:"\f3aa"}.bi-file-plus::before{content:"\f3ab"}.bi-file-post-fill::before{content:"\f3ac"}.bi-file-post::before{content:"\f3ad"}.bi-file-ppt-fill::before{content:"\f3ae"}.bi-file-ppt::before{content:"\f3af"}.bi-file-richtext-fill::before{content:"\f3b0"}.bi-file-richtext::before{content:"\f3b1"}.bi-file-ruled-fill::before{content:"\f3b2"}.bi-file-ruled::before{content:"\f3b3"}.bi-file-slides-fill::before{content:"\f3b4"}.bi-file-slides::before{content:"\f3b5"}.bi-file-spreadsheet-fill::before{content:"\f3b6"}.bi-file-spreadsheet::before{content:"\f3b7"}.bi-file-text-fill::before{content:"\f3b8"}.bi-file-text::before{content:"\f3b9"}.bi-file-word-fill::before{content:"\f3ba"}.bi-file-word::before{content:"\f3bb"}.bi-file-x-fill::before{content:"\f3bc"}.bi-file-x::before{content:"\f3bd"}.bi-file-zip-fill::before{content:"\f3be"}.bi-file-zip::before{content:"\f3bf"}.bi-file::before{content:"\f3c0"}.bi-files-alt::before{content:"\f3c1"}.bi-files::before{content:"\f3c2"}.bi-film::before{content:"\f3c3"}.bi-filter-circle-fill::before{content:"\f3c4"}.bi-filter-circle::before{content:"\f3c5"}.bi-filter-left::before{content:"\f3c6"}.bi-filter-right::before{content:"\f3c7"}.bi-filter-square-fill::before{content:"\f3c8"}.bi-filter-square::before{content:"\f3c9"}.bi-filter::before{content:"\f3ca"}.bi-flag-fill::before{content:"\f3cb"}.bi-flag::before{content:"\f3cc"}.bi-flower1::before{content:"\f3cd"}.bi-flower2::before{content:"\f3ce"}.bi-flower3::before{content:"\f3cf"}.bi-folder-check::before{content:"\f3d0"}.bi-folder-fill::before{content:"\f3d1"}.bi-folder-minus::before{content:"\f3d2"}.bi-folder-plus::before{content:"\f3d3"}.bi-folder-symlink-fill::before{content:"\f3d4"}.bi-folder-symlink::before{content:"\f3d5"}.bi-folder-x::before{content:"\f3d6"}.bi-folder::before{content:"\f3d7"}.bi-folder2-open::before{content:"\f3d8"}.bi-folder2::before{content:"\f3d9"}.bi-fonts::before{content:"\f3da"}.bi-forward-fill::before{content:"\f3db"}.bi-forward::before{content:"\f3dc"}.bi-front::before{content:"\f3dd"}.bi-fullscreen-exit::before{content:"\f3de"}.bi-fullscreen::before{content:"\f3df"}.bi-funnel-fill::before{content:"\f3e0"}.bi-funnel::before{content:"\f3e1"}.bi-gear-fill::before{content:"\f3e2"}.bi-gear-wide-connected::before{content:"\f3e3"}.bi-gear-wide::before{content:"\f3e4"}.bi-gear::before{content:"\f3e5"}.bi-gem::before{content:"\f3e6"}.bi-geo-alt-fill::before{content:"\f3e7"}.bi-geo-alt::before{content:"\f3e8"}.bi-geo-fill::before{content:"\f3e9"}.bi-geo::before{content:"\f3ea"}.bi-gift-fill::before{content:"\f3eb"}.bi-gift::before{content:"\f3ec"}.bi-github::before{content:"\f3ed"}.bi-globe::before{content:"\f3ee"}.bi-globe2::before{content:"\f3ef"}.bi-google::before{content:"\f3f0"}.bi-graph-down::before{content:"\f3f1"}.bi-graph-up::before{content:"\f3f2"}.bi-grid-1x2-fill::before{content:"\f3f3"}.bi-grid-1x2::before{content:"\f3f4"}.bi-grid-3x2-gap-fill::before{content:"\f3f5"}.bi-grid-3x2-gap::before{content:"\f3f6"}.bi-grid-3x2::before{content:"\f3f7"}.bi-grid-3x3-gap-fill::before{content:"\f3f8"}.bi-grid-3x3-gap::before{content:"\f3f9"}.bi-grid-3x3::before{content:"\f3fa"}.bi-grid-fill::before{content:"\f3fb"}.bi-grid::before{content:"\f3fc"}.bi-grip-horizontal::before{content:"\f3fd"}.bi-grip-vertical::before{content:"\f3fe"}.bi-hammer::before{content:"\f3ff"}.bi-hand-index-fill::before{content:"\f400"}.bi-hand-index-thumb-fill::before{content:"\f401"}.bi-hand-index-thumb::before{content:"\f402"}.bi-hand-index::before{content:"\f403"}.bi-hand-thumbs-down-fill::before{content:"\f404"}.bi-hand-thumbs-down::before{content:"\f405"}.bi-hand-thumbs-up-fill::before{content:"\f406"}.bi-hand-thumbs-up::before{content:"\f407"}.bi-handbag-fill::before{content:"\f408"}.bi-handbag::before{content:"\f409"}.bi-hash::before{content:"\f40a"}.bi-hdd-fill::before{content:"\f40b"}.bi-hdd-network-fill::before{content:"\f40c"}.bi-hdd-network::before{content:"\f40d"}.bi-hdd-rack-fill::before{content:"\f40e"}.bi-hdd-rack::before{content:"\f40f"}.bi-hdd-stack-fill::before{content:"\f410"}.bi-hdd-stack::before{content:"\f411"}.bi-hdd::before{content:"\f412"}.bi-headphones::before{content:"\f413"}.bi-headset::before{content:"\f414"}.bi-heart-fill::before{content:"\f415"}.bi-heart-half::before{content:"\f416"}.bi-heart::before{content:"\f417"}.bi-heptagon-fill::before{content:"\f418"}.bi-heptagon-half::before{content:"\f419"}.bi-heptagon::before{content:"\f41a"}.bi-hexagon-fill::before{content:"\f41b"}.bi-hexagon-half::before{content:"\f41c"}.bi-hexagon::before{content:"\f41d"}.bi-hourglass-bottom::before{content:"\f41e"}.bi-hourglass-split::before{content:"\f41f"}.bi-hourglass-top::before{content:"\f420"}.bi-hourglass::before{content:"\f421"}.bi-house-door-fill::before{content:"\f422"}.bi-house-door::before{content:"\f423"}.bi-house-fill::before{content:"\f424"}.bi-house::before{content:"\f425"}.bi-hr::before{content:"\f426"}.bi-hurricane::before{content:"\f427"}.bi-image-alt::before{content:"\f428"}.bi-image-fill::before{content:"\f429"}.bi-image::before{content:"\f42a"}.bi-images::before{content:"\f42b"}.bi-inbox-fill::before{content:"\f42c"}.bi-inbox::before{content:"\f42d"}.bi-inboxes-fill::before{content:"\f42e"}.bi-inboxes::before{content:"\f42f"}.bi-info-circle-fill::before{content:"\f430"}.bi-info-circle::before{content:"\f431"}.bi-info-square-fill::before{content:"\f432"}.bi-info-square::before{content:"\f433"}.bi-info::before{content:"\f434"}.bi-input-cursor-text::before{content:"\f435"}.bi-input-cursor::before{content:"\f436"}.bi-instagram::before{content:"\f437"}.bi-intersect::before{content:"\f438"}.bi-journal-album::before{content:"\f439"}.bi-journal-arrow-down::before{content:"\f43a"}.bi-journal-arrow-up::before{content:"\f43b"}.bi-journal-bookmark-fill::before{content:"\f43c"}.bi-journal-bookmark::before{content:"\f43d"}.bi-journal-check::before{content:"\f43e"}.bi-journal-code::before{content:"\f43f"}.bi-journal-medical::before{content:"\f440"}.bi-journal-minus::before{content:"\f441"}.bi-journal-plus::before{content:"\f442"}.bi-journal-richtext::before{content:"\f443"}.bi-journal-text::before{content:"\f444"}.bi-journal-x::before{content:"\f445"}.bi-journal::before{content:"\f446"}.bi-journals::before{content:"\f447"}.bi-joystick::before{content:"\f448"}.bi-justify-left::before{content:"\f449"}.bi-justify-right::before{content:"\f44a"}.bi-justify::before{content:"\f44b"}.bi-kanban-fill::before{content:"\f44c"}.bi-kanban::before{content:"\f44d"}.bi-key-fill::before{content:"\f44e"}.bi-key::before{content:"\f44f"}.bi-keyboard-fill::before{content:"\f450"}.bi-keyboard::before{content:"\f451"}.bi-ladder::before{content:"\f452"}.bi-lamp-fill::before{content:"\f453"}.bi-lamp::before{content:"\f454"}.bi-laptop-fill::before{content:"\f455"}.bi-laptop::before{content:"\f456"}.bi-layer-backward::before{content:"\f457"}.bi-layer-forward::before{content:"\f458"}.bi-layers-fill::before{content:"\f459"}.bi-layers-half::before{content:"\f45a"}.bi-layers::before{content:"\f45b"}.bi-layout-sidebar-inset-reverse::before{content:"\f45c"}.bi-layout-sidebar-inset::before{content:"\f45d"}.bi-layout-sidebar-reverse::before{content:"\f45e"}.bi-layout-sidebar::before{content:"\f45f"}.bi-layout-split::before{content:"\f460"}.bi-layout-text-sidebar-reverse::before{content:"\f461"}.bi-layout-text-sidebar::before{content:"\f462"}.bi-layout-text-window-reverse::before{content:"\f463"}.bi-layout-text-window::before{content:"\f464"}.bi-layout-three-columns::before{content:"\f465"}.bi-layout-wtf::before{content:"\f466"}.bi-life-preserver::before{content:"\f467"}.bi-lightbulb-fill::before{content:"\f468"}.bi-lightbulb-off-fill::before{content:"\f469"}.bi-lightbulb-off::before{content:"\f46a"}.bi-lightbulb::before{content:"\f46b"}.bi-lightning-charge-fill::before{content:"\f46c"}.bi-lightning-charge::before{content:"\f46d"}.bi-lightning-fill::before{content:"\f46e"}.bi-lightning::before{content:"\f46f"}.bi-link-45deg::before{content:"\f470"}.bi-link::before{content:"\f471"}.bi-linkedin::before{content:"\f472"}.bi-list-check::before{content:"\f473"}.bi-list-nested::before{content:"\f474"}.bi-list-ol::before{content:"\f475"}.bi-list-stars::before{content:"\f476"}.bi-list-task::before{content:"\f477"}.bi-list-ul::before{content:"\f478"}.bi-list::before{content:"\f479"}.bi-lock-fill::before{content:"\f47a"}.bi-lock::before{content:"\f47b"}.bi-mailbox::before{content:"\f47c"}.bi-mailbox2::before{content:"\f47d"}.bi-map-fill::before{content:"\f47e"}.bi-map::before{content:"\f47f"}.bi-markdown-fill::before{content:"\f480"}.bi-markdown::before{content:"\f481"}.bi-mask::before{content:"\f482"}.bi-megaphone-fill::before{content:"\f483"}.bi-megaphone::before{content:"\f484"}.bi-menu-app-fill::before{content:"\f485"}.bi-menu-app::before{content:"\f486"}.bi-menu-button-fill::before{content:"\f487"}.bi-menu-button-wide-fill::before{content:"\f488"}.bi-menu-button-wide::before{content:"\f489"}.bi-menu-button::before{content:"\f48a"}.bi-menu-down::before{content:"\f48b"}.bi-menu-up::before{content:"\f48c"}.bi-mic-fill::before{content:"\f48d"}.bi-mic-mute-fill::before{content:"\f48e"}.bi-mic-mute::before{content:"\f48f"}.bi-mic::before{content:"\f490"}.bi-minecart-loaded::before{content:"\f491"}.bi-minecart::before{content:"\f492"}.bi-moisture::before{content:"\f493"}.bi-moon-fill::before{content:"\f494"}.bi-moon-stars-fill::before{content:"\f495"}.bi-moon-stars::before{content:"\f496"}.bi-moon::before{content:"\f497"}.bi-mouse-fill::before{content:"\f498"}.bi-mouse::before{content:"\f499"}.bi-mouse2-fill::before{content:"\f49a"}.bi-mouse2::before{content:"\f49b"}.bi-mouse3-fill::before{content:"\f49c"}.bi-mouse3::before{content:"\f49d"}.bi-music-note-beamed::before{content:"\f49e"}.bi-music-note-list::before{content:"\f49f"}.bi-music-note::before{content:"\f4a0"}.bi-music-player-fill::before{content:"\f4a1"}.bi-music-player::before{content:"\f4a2"}.bi-newspaper::before{content:"\f4a3"}.bi-node-minus-fill::before{content:"\f4a4"}.bi-node-minus::before{content:"\f4a5"}.bi-node-plus-fill::before{content:"\f4a6"}.bi-node-plus::before{content:"\f4a7"}.bi-nut-fill::before{content:"\f4a8"}.bi-nut::before{content:"\f4a9"}.bi-octagon-fill::before{content:"\f4aa"}.bi-octagon-half::before{content:"\f4ab"}.bi-octagon::before{content:"\f4ac"}.bi-option::before{content:"\f4ad"}.bi-outlet::before{content:"\f4ae"}.bi-paint-bucket::before{content:"\f4af"}.bi-palette-fill::before{content:"\f4b0"}.bi-palette::before{content:"\f4b1"}.bi-palette2::before{content:"\f4b2"}.bi-paperclip::before{content:"\f4b3"}.bi-paragraph::before{content:"\f4b4"}.bi-patch-check-fill::before{content:"\f4b5"}.bi-patch-check::before{content:"\f4b6"}.bi-patch-exclamation-fill::before{content:"\f4b7"}.bi-patch-exclamation::before{content:"\f4b8"}.bi-patch-minus-fill::before{content:"\f4b9"}.bi-patch-minus::before{content:"\f4ba"}.bi-patch-plus-fill::before{content:"\f4bb"}.bi-patch-plus::before{content:"\f4bc"}.bi-patch-question-fill::before{content:"\f4bd"}.bi-patch-question::before{content:"\f4be"}.bi-pause-btn-fill::before{content:"\f4bf"}.bi-pause-btn::before{content:"\f4c0"}.bi-pause-circle-fill::before{content:"\f4c1"}.bi-pause-circle::before{content:"\f4c2"}.bi-pause-fill::before{content:"\f4c3"}.bi-pause::before{content:"\f4c4"}.bi-peace-fill::before{content:"\f4c5"}.bi-peace::before{content:"\f4c6"}.bi-pen-fill::before{content:"\f4c7"}.bi-pen::before{content:"\f4c8"}.bi-pencil-fill::before{content:"\f4c9"}.bi-pencil-square::before{content:"\f4ca"}.bi-pencil::before{content:"\f4cb"}.bi-pentagon-fill::before{content:"\f4cc"}.bi-pentagon-half::before{content:"\f4cd"}.bi-pentagon::before{content:"\f4ce"}.bi-people-fill::before{content:"\f4cf"}.bi-people::before{content:"\f4d0"}.bi-percent::before{content:"\f4d1"}.bi-person-badge-fill::before{content:"\f4d2"}.bi-person-badge::before{content:"\f4d3"}.bi-person-bounding-box::before{content:"\f4d4"}.bi-person-check-fill::before{content:"\f4d5"}.bi-person-check::before{content:"\f4d6"}.bi-person-circle::before{content:"\f4d7"}.bi-person-dash-fill::before{content:"\f4d8"}.bi-person-dash::before{content:"\f4d9"}.bi-person-fill::before{content:"\f4da"}.bi-person-lines-fill::before{content:"\f4db"}.bi-person-plus-fill::before{content:"\f4dc"}.bi-person-plus::before{content:"\f4dd"}.bi-person-square::before{content:"\f4de"}.bi-person-x-fill::before{content:"\f4df"}.bi-person-x::before{content:"\f4e0"}.bi-person::before{content:"\f4e1"}.bi-phone-fill::before{content:"\f4e2"}.bi-phone-landscape-fill::before{content:"\f4e3"}.bi-phone-landscape::before{content:"\f4e4"}.bi-phone-vibrate-fill::before{content:"\f4e5"}.bi-phone-vibrate::before{content:"\f4e6"}.bi-phone::before{content:"\f4e7"}.bi-pie-chart-fill::before{content:"\f4e8"}.bi-pie-chart::before{content:"\f4e9"}.bi-pin-angle-fill::before{content:"\f4ea"}.bi-pin-angle::before{content:"\f4eb"}.bi-pin-fill::before{content:"\f4ec"}.bi-pin::before{content:"\f4ed"}.bi-pip-fill::before{content:"\f4ee"}.bi-pip::before{content:"\f4ef"}.bi-play-btn-fill::before{content:"\f4f0"}.bi-play-btn::before{content:"\f4f1"}.bi-play-circle-fill::before{content:"\f4f2"}.bi-play-circle::before{content:"\f4f3"}.bi-play-fill::before{content:"\f4f4"}.bi-play::before{content:"\f4f5"}.bi-plug-fill::before{content:"\f4f6"}.bi-plug::before{content:"\f4f7"}.bi-plus-circle-dotted::before{content:"\f4f8"}.bi-plus-circle-fill::before{content:"\f4f9"}.bi-plus-circle::before{content:"\f4fa"}.bi-plus-square-dotted::before{content:"\f4fb"}.bi-plus-square-fill::before{content:"\f4fc"}.bi-plus-square::before{content:"\f4fd"}.bi-plus::before{content:"\f4fe"}.bi-power::before{content:"\f4ff"}.bi-printer-fill::before{content:"\f500"}.bi-printer::before{content:"\f501"}.bi-puzzle-fill::before{content:"\f502"}.bi-puzzle::before{content:"\f503"}.bi-question-circle-fill::before{content:"\f504"}.bi-question-circle::before{content:"\f505"}.bi-question-diamond-fill::before{content:"\f506"}.bi-question-diamond::before{content:"\f507"}.bi-question-octagon-fill::before{content:"\f508"}.bi-question-octagon::before{content:"\f509"}.bi-question-square-fill::before{content:"\f50a"}.bi-question-square::before{content:"\f50b"}.bi-question::before{content:"\f50c"}.bi-rainbow::before{content:"\f50d"}.bi-receipt-cutoff::before{content:"\f50e"}.bi-receipt::before{content:"\f50f"}.bi-reception-0::before{content:"\f510"}.bi-reception-1::before{content:"\f511"}.bi-reception-2::before{content:"\f512"}.bi-reception-3::before{content:"\f513"}.bi-reception-4::before{content:"\f514"}.bi-record-btn-fill::before{content:"\f515"}.bi-record-btn::before{content:"\f516"}.bi-record-circle-fill::before{content:"\f517"}.bi-record-circle::before{content:"\f518"}.bi-record-fill::before{content:"\f519"}.bi-record::before{content:"\f51a"}.bi-record2-fill::before{content:"\f51b"}.bi-record2::before{content:"\f51c"}.bi-reply-all-fill::before{content:"\f51d"}.bi-reply-all::before{content:"\f51e"}.bi-reply-fill::before{content:"\f51f"}.bi-reply::before{content:"\f520"}.bi-rss-fill::before{content:"\f521"}.bi-rss::before{content:"\f522"}.bi-rulers::before{content:"\f523"}.bi-save-fill::before{content:"\f524"}.bi-save::before{content:"\f525"}.bi-save2-fill::before{content:"\f526"}.bi-save2::before{content:"\f527"}.bi-scissors::before{content:"\f528"}.bi-screwdriver::before{content:"\f529"}.bi-search::before{content:"\f52a"}.bi-segmented-nav::before{content:"\f52b"}.bi-server::before{content:"\f52c"}.bi-share-fill::before{content:"\f52d"}.bi-share::before{content:"\f52e"}.bi-shield-check::before{content:"\f52f"}.bi-shield-exclamation::before{content:"\f530"}.bi-shield-fill-check::before{content:"\f531"}.bi-shield-fill-exclamation::before{content:"\f532"}.bi-shield-fill-minus::before{content:"\f533"}.bi-shield-fill-plus::before{content:"\f534"}.bi-shield-fill-x::before{content:"\f535"}.bi-shield-fill::before{content:"\f536"}.bi-shield-lock-fill::before{content:"\f537"}.bi-shield-lock::before{content:"\f538"}.bi-shield-minus::before{content:"\f539"}.bi-shield-plus::before{content:"\f53a"}.bi-shield-shaded::before{content:"\f53b"}.bi-shield-slash-fill::before{content:"\f53c"}.bi-shield-slash::before{content:"\f53d"}.bi-shield-x::before{content:"\f53e"}.bi-shield::before{content:"\f53f"}.bi-shift-fill::before{content:"\f540"}.bi-shift::before{content:"\f541"}.bi-shop-window::before{content:"\f542"}.bi-shop::before{content:"\f543"}.bi-shuffle::before{content:"\f544"}.bi-signpost-2-fill::before{content:"\f545"}.bi-signpost-2::before{content:"\f546"}.bi-signpost-fill::before{content:"\f547"}.bi-signpost-split-fill::before{content:"\f548"}.bi-signpost-split::before{content:"\f549"}.bi-signpost::before{content:"\f54a"}.bi-sim-fill::before{content:"\f54b"}.bi-sim::before{content:"\f54c"}.bi-skip-backward-btn-fill::before{content:"\f54d"}.bi-skip-backward-btn::before{content:"\f54e"}.bi-skip-backward-circle-fill::before{content:"\f54f"}.bi-skip-backward-circle::before{content:"\f550"}.bi-skip-backward-fill::before{content:"\f551"}.bi-skip-backward::before{content:"\f552"}.bi-skip-end-btn-fill::before{content:"\f553"}.bi-skip-end-btn::before{content:"\f554"}.bi-skip-end-circle-fill::before{content:"\f555"}.bi-skip-end-circle::before{content:"\f556"}.bi-skip-end-fill::before{content:"\f557"}.bi-skip-end::before{content:"\f558"}.bi-skip-forward-btn-fill::before{content:"\f559"}.bi-skip-forward-btn::before{content:"\f55a"}.bi-skip-forward-circle-fill::before{content:"\f55b"}.bi-skip-forward-circle::before{content:"\f55c"}.bi-skip-forward-fill::before{content:"\f55d"}.bi-skip-forward::before{content:"\f55e"}.bi-skip-start-btn-fill::before{content:"\f55f"}.bi-skip-start-btn::before{content:"\f560"}.bi-skip-start-circle-fill::before{content:"\f561"}.bi-skip-start-circle::before{content:"\f562"}.bi-skip-start-fill::before{content:"\f563"}.bi-skip-start::before{content:"\f564"}.bi-slack::before{content:"\f565"}.bi-slash-circle-fill::before{content:"\f566"}.bi-slash-circle::before{content:"\f567"}.bi-slash-square-fill::before{content:"\f568"}.bi-slash-square::before{content:"\f569"}.bi-slash::before{content:"\f56a"}.bi-sliders::before{content:"\f56b"}.bi-smartwatch::before{content:"\f56c"}.bi-snow::before{content:"\f56d"}.bi-snow2::before{content:"\f56e"}.bi-snow3::before{content:"\f56f"}.bi-sort-alpha-down-alt::before{content:"\f570"}.bi-sort-alpha-down::before{content:"\f571"}.bi-sort-alpha-up-alt::before{content:"\f572"}.bi-sort-alpha-up::before{content:"\f573"}.bi-sort-down-alt::before{content:"\f574"}.bi-sort-down::before{content:"\f575"}.bi-sort-numeric-down-alt::before{content:"\f576"}.bi-sort-numeric-down::before{content:"\f577"}.bi-sort-numeric-up-alt::before{content:"\f578"}.bi-sort-numeric-up::before{content:"\f579"}.bi-sort-up-alt::before{content:"\f57a"}.bi-sort-up::before{content:"\f57b"}.bi-soundwave::before{content:"\f57c"}.bi-speaker-fill::before{content:"\f57d"}.bi-speaker::before{content:"\f57e"}.bi-speedometer::before{content:"\f57f"}.bi-speedometer2::before{content:"\f580"}.bi-spellcheck::before{content:"\f581"}.bi-square-fill::before{content:"\f582"}.bi-square-half::before{content:"\f583"}.bi-square::before{content:"\f584"}.bi-stack::before{content:"\f585"}.bi-star-fill::before{content:"\f586"}.bi-star-half::before{content:"\f587"}.bi-star::before{content:"\f588"}.bi-stars::before{content:"\f589"}.bi-stickies-fill::before{content:"\f58a"}.bi-stickies::before{content:"\f58b"}.bi-sticky-fill::before{content:"\f58c"}.bi-sticky::before{content:"\f58d"}.bi-stop-btn-fill::before{content:"\f58e"}.bi-stop-btn::before{content:"\f58f"}.bi-stop-circle-fill::before{content:"\f590"}.bi-stop-circle::before{content:"\f591"}.bi-stop-fill::before{content:"\f592"}.bi-stop::before{content:"\f593"}.bi-stoplights-fill::before{content:"\f594"}.bi-stoplights::before{content:"\f595"}.bi-stopwatch-fill::before{content:"\f596"}.bi-stopwatch::before{content:"\f597"}.bi-subtract::before{content:"\f598"}.bi-suit-club-fill::before{content:"\f599"}.bi-suit-club::before{content:"\f59a"}.bi-suit-diamond-fill::before{content:"\f59b"}.bi-suit-diamond::before{content:"\f59c"}.bi-suit-heart-fill::before{content:"\f59d"}.bi-suit-heart::before{content:"\f59e"}.bi-suit-spade-fill::before{content:"\f59f"}.bi-suit-spade::before{content:"\f5a0"}.bi-sun-fill::before{content:"\f5a1"}.bi-sun::before{content:"\f5a2"}.bi-sunglasses::before{content:"\f5a3"}.bi-sunrise-fill::before{content:"\f5a4"}.bi-sunrise::before{content:"\f5a5"}.bi-sunset-fill::before{content:"\f5a6"}.bi-sunset::before{content:"\f5a7"}.bi-symmetry-horizontal::before{content:"\f5a8"}.bi-symmetry-vertical::before{content:"\f5a9"}.bi-table::before{content:"\f5aa"}.bi-tablet-fill::before{content:"\f5ab"}.bi-tablet-landscape-fill::before{content:"\f5ac"}.bi-tablet-landscape::before{content:"\f5ad"}.bi-tablet::before{content:"\f5ae"}.bi-tag-fill::before{content:"\f5af"}.bi-tag::before{content:"\f5b0"}.bi-tags-fill::before{content:"\f5b1"}.bi-tags::before{content:"\f5b2"}.bi-telegram::before{content:"\f5b3"}.bi-telephone-fill::before{content:"\f5b4"}.bi-telephone-forward-fill::before{content:"\f5b5"}.bi-telephone-forward::before{content:"\f5b6"}.bi-telephone-inbound-fill::before{content:"\f5b7"}.bi-telephone-inbound::before{content:"\f5b8"}.bi-telephone-minus-fill::before{content:"\f5b9"}.bi-telephone-minus::before{content:"\f5ba"}.bi-telephone-outbound-fill::before{content:"\f5bb"}.bi-telephone-outbound::before{content:"\f5bc"}.bi-telephone-plus-fill::before{content:"\f5bd"}.bi-telephone-plus::before{content:"\f5be"}.bi-telephone-x-fill::before{content:"\f5bf"}.bi-telephone-x::before{content:"\f5c0"}.bi-telephone::before{content:"\f5c1"}.bi-terminal-fill::before{content:"\f5c2"}.bi-terminal::before{content:"\f5c3"}.bi-text-center::before{content:"\f5c4"}.bi-text-indent-left::before{content:"\f5c5"}.bi-text-indent-right::before{content:"\f5c6"}.bi-text-left::before{content:"\f5c7"}.bi-text-paragraph::before{content:"\f5c8"}.bi-text-right::before{content:"\f5c9"}.bi-textarea-resize::before{content:"\f5ca"}.bi-textarea-t::before{content:"\f5cb"}.bi-textarea::before{content:"\f5cc"}.bi-thermometer-half::before{content:"\f5cd"}.bi-thermometer-high::before{content:"\f5ce"}.bi-thermometer-low::before{content:"\f5cf"}.bi-thermometer-snow::before{content:"\f5d0"}.bi-thermometer-sun::before{content:"\f5d1"}.bi-thermometer::before{content:"\f5d2"}.bi-three-dots-vertical::before{content:"\f5d3"}.bi-three-dots::before{content:"\f5d4"}.bi-toggle-off::before{content:"\f5d5"}.bi-toggle-on::before{content:"\f5d6"}.bi-toggle2-off::before{content:"\f5d7"}.bi-toggle2-on::before{content:"\f5d8"}.bi-toggles::before{content:"\f5d9"}.bi-toggles2::before{content:"\f5da"}.bi-tools::before{content:"\f5db"}.bi-tornado::before{content:"\f5dc"}.bi-trash-fill::before{content:"\f5dd"}.bi-trash::before{content:"\f5de"}.bi-trash2-fill::before{content:"\f5df"}.bi-trash2::before{content:"\f5e0"}.bi-tree-fill::before{content:"\f5e1"}.bi-tree::before{content:"\f5e2"}.bi-triangle-fill::before{content:"\f5e3"}.bi-triangle-half::before{content:"\f5e4"}.bi-triangle::before{content:"\f5e5"}.bi-trophy-fill::before{content:"\f5e6"}.bi-trophy::before{content:"\f5e7"}.bi-tropical-storm::before{content:"\f5e8"}.bi-truck-flatbed::before{content:"\f5e9"}.bi-truck::before{content:"\f5ea"}.bi-tsunami::before{content:"\f5eb"}.bi-tv-fill::before{content:"\f5ec"}.bi-tv::before{content:"\f5ed"}.bi-twitch::before{content:"\f5ee"}.bi-twitter::before{content:"\f5ef"}.bi-type-bold::before{content:"\f5f0"}.bi-type-h1::before{content:"\f5f1"}.bi-type-h2::before{content:"\f5f2"}.bi-type-h3::before{content:"\f5f3"}.bi-type-italic::before{content:"\f5f4"}.bi-type-strikethrough::before{content:"\f5f5"}.bi-type-underline::before{content:"\f5f6"}.bi-type::before{content:"\f5f7"}.bi-ui-checks-grid::before{content:"\f5f8"}.bi-ui-checks::before{content:"\f5f9"}.bi-ui-radios-grid::before{content:"\f5fa"}.bi-ui-radios::before{content:"\f5fb"}.bi-umbrella-fill::before{content:"\f5fc"}.bi-umbrella::before{content:"\f5fd"}.bi-union::before{content:"\f5fe"}.bi-unlock-fill::before{content:"\f5ff"}.bi-unlock::before{content:"\f600"}.bi-upc-scan::before{content:"\f601"}.bi-upc::before{content:"\f602"}.bi-upload::before{content:"\f603"}.bi-vector-pen::before{content:"\f604"}.bi-view-list::before{content:"\f605"}.bi-view-stacked::before{content:"\f606"}.bi-vinyl-fill::before{content:"\f607"}.bi-vinyl::before{content:"\f608"}.bi-voicemail::before{content:"\f609"}.bi-volume-down-fill::before{content:"\f60a"}.bi-volume-down::before{content:"\f60b"}.bi-volume-mute-fill::before{content:"\f60c"}.bi-volume-mute::before{content:"\f60d"}.bi-volume-off-fill::before{content:"\f60e"}.bi-volume-off::before{content:"\f60f"}.bi-volume-up-fill::before{content:"\f610"}.bi-volume-up::before{content:"\f611"}.bi-vr::before{content:"\f612"}.bi-wallet-fill::before{content:"\f613"}.bi-wallet::before{content:"\f614"}.bi-wallet2::before{content:"\f615"}.bi-watch::before{content:"\f616"}.bi-water::before{content:"\f617"}.bi-whatsapp::before{content:"\f618"}.bi-wifi-1::before{content:"\f619"}.bi-wifi-2::before{content:"\f61a"}.bi-wifi-off::before{content:"\f61b"}.bi-wifi::before{content:"\f61c"}.bi-wind::before{content:"\f61d"}.bi-window-dock::before{content:"\f61e"}.bi-window-sidebar::before{content:"\f61f"}.bi-window::before{content:"\f620"}.bi-wrench::before{content:"\f621"}.bi-x-circle-fill::before{content:"\f622"}.bi-x-circle::before{content:"\f623"}.bi-x-diamond-fill::before{content:"\f624"}.bi-x-diamond::before{content:"\f625"}.bi-x-octagon-fill::before{content:"\f626"}.bi-x-octagon::before{content:"\f627"}.bi-x-square-fill::before{content:"\f628"}.bi-x-square::before{content:"\f629"}.bi-x::before{content:"\f62a"}.bi-youtube::before{content:"\f62b"}.bi-zoom-in::before{content:"\f62c"}.bi-zoom-out::before{content:"\f62d"}.bi-bank::before{content:"\f62e"}.bi-bank2::before{content:"\f62f"}.bi-bell-slash-fill::before{content:"\f630"}.bi-bell-slash::before{content:"\f631"}.bi-cash-coin::before{content:"\f632"}.bi-check-lg::before{content:"\f633"}.bi-coin::before{content:"\f634"}.bi-currency-bitcoin::before{content:"\f635"}.bi-currency-dollar::before{content:"\f636"}.bi-currency-euro::before{content:"\f637"}.bi-currency-exchange::before{content:"\f638"}.bi-currency-pound::before{content:"\f639"}.bi-currency-yen::before{content:"\f63a"}.bi-dash-lg::before{content:"\f63b"}.bi-exclamation-lg::before{content:"\f63c"}.bi-file-earmark-pdf-fill::before{content:"\f63d"}.bi-file-earmark-pdf::before{content:"\f63e"}.bi-file-pdf-fill::before{content:"\f63f"}.bi-file-pdf::before{content:"\f640"}.bi-gender-ambiguous::before{content:"\f641"}.bi-gender-female::before{content:"\f642"}.bi-gender-male::before{content:"\f643"}.bi-gender-trans::before{content:"\f644"}.bi-headset-vr::before{content:"\f645"}.bi-info-lg::before{content:"\f646"}.bi-mastodon::before{content:"\f647"}.bi-messenger::before{content:"\f648"}.bi-piggy-bank-fill::before{content:"\f649"}.bi-piggy-bank::before{content:"\f64a"}.bi-pin-map-fill::before{content:"\f64b"}.bi-pin-map::before{content:"\f64c"}.bi-plus-lg::before{content:"\f64d"}.bi-question-lg::before{content:"\f64e"}.bi-recycle::before{content:"\f64f"}.bi-reddit::before{content:"\f650"}.bi-safe-fill::before{content:"\f651"}.bi-safe2-fill::before{content:"\f652"}.bi-safe2::before{content:"\f653"}.bi-sd-card-fill::before{content:"\f654"}.bi-sd-card::before{content:"\f655"}.bi-skype::before{content:"\f656"}.bi-slash-lg::before{content:"\f657"}.bi-translate::before{content:"\f658"}.bi-x-lg::before{content:"\f659"}.bi-safe::before{content:"\f65a"}.bi-apple::before{content:"\f65b"}.bi-microsoft::before{content:"\f65d"}.bi-windows::before{content:"\f65e"}.bi-behance::before{content:"\f65c"}.bi-dribbble::before{content:"\f65f"}.bi-line::before{content:"\f660"}.bi-medium::before{content:"\f661"}.bi-paypal::before{content:"\f662"}.bi-pinterest::before{content:"\f663"}.bi-signal::before{content:"\f664"}.bi-snapchat::before{content:"\f665"}.bi-spotify::before{content:"\f666"}.bi-stack-overflow::before{content:"\f667"}.bi-strava::before{content:"\f668"}.bi-wordpress::before{content:"\f669"}.bi-vimeo::before{content:"\f66a"}.bi-activity::before{content:"\f66b"}.bi-easel2-fill::before{content:"\f66c"}.bi-easel2::before{content:"\f66d"}.bi-easel3-fill::before{content:"\f66e"}.bi-easel3::before{content:"\f66f"}.bi-fan::before{content:"\f670"}.bi-fingerprint::before{content:"\f671"}.bi-graph-down-arrow::before{content:"\f672"}.bi-graph-up-arrow::before{content:"\f673"}.bi-hypnotize::before{content:"\f674"}.bi-magic::before{content:"\f675"}.bi-person-rolodex::before{content:"\f676"}.bi-person-video::before{content:"\f677"}.bi-person-video2::before{content:"\f678"}.bi-person-video3::before{content:"\f679"}.bi-person-workspace::before{content:"\f67a"}.bi-radioactive::before{content:"\f67b"}.bi-webcam-fill::before{content:"\f67c"}.bi-webcam::before{content:"\f67d"}.bi-yin-yang::before{content:"\f67e"}.bi-bandaid-fill::before{content:"\f680"}.bi-bandaid::before{content:"\f681"}.bi-bluetooth::before{content:"\f682"}.bi-body-text::before{content:"\f683"}.bi-boombox::before{content:"\f684"}.bi-boxes::before{content:"\f685"}.bi-dpad-fill::before{content:"\f686"}.bi-dpad::before{content:"\f687"}.bi-ear-fill::before{content:"\f688"}.bi-ear::before{content:"\f689"}.bi-envelope-check-fill::before{content:"\f68b"}.bi-envelope-check::before{content:"\f68c"}.bi-envelope-dash-fill::before{content:"\f68e"}.bi-envelope-dash::before{content:"\f68f"}.bi-envelope-exclamation-fill::before{content:"\f691"}.bi-envelope-exclamation::before{content:"\f692"}.bi-envelope-plus-fill::before{content:"\f693"}.bi-envelope-plus::before{content:"\f694"}.bi-envelope-slash-fill::before{content:"\f696"}.bi-envelope-slash::before{content:"\f697"}.bi-envelope-x-fill::before{content:"\f699"}.bi-envelope-x::before{content:"\f69a"}.bi-explicit-fill::before{content:"\f69b"}.bi-explicit::before{content:"\f69c"}.bi-git::before{content:"\f69d"}.bi-infinity::before{content:"\f69e"}.bi-list-columns-reverse::before{content:"\f69f"}.bi-list-columns::before{content:"\f6a0"}.bi-meta::before{content:"\f6a1"}.bi-nintendo-switch::before{content:"\f6a4"}.bi-pc-display-horizontal::before{content:"\f6a5"}.bi-pc-display::before{content:"\f6a6"}.bi-pc-horizontal::before{content:"\f6a7"}.bi-pc::before{content:"\f6a8"}.bi-playstation::before{content:"\f6a9"}.bi-plus-slash-minus::before{content:"\f6aa"}.bi-projector-fill::before{content:"\f6ab"}.bi-projector::before{content:"\f6ac"}.bi-qr-code-scan::before{content:"\f6ad"}.bi-qr-code::before{content:"\f6ae"}.bi-quora::before{content:"\f6af"}.bi-quote::before{content:"\f6b0"}.bi-robot::before{content:"\f6b1"}.bi-send-check-fill::before{content:"\f6b2"}.bi-send-check::before{content:"\f6b3"}.bi-send-dash-fill::before{content:"\f6b4"}.bi-send-dash::before{content:"\f6b5"}.bi-send-exclamation-fill::before{content:"\f6b7"}.bi-send-exclamation::before{content:"\f6b8"}.bi-send-fill::before{content:"\f6b9"}.bi-send-plus-fill::before{content:"\f6ba"}.bi-send-plus::before{content:"\f6bb"}.bi-send-slash-fill::before{content:"\f6bc"}.bi-send-slash::before{content:"\f6bd"}.bi-send-x-fill::before{content:"\f6be"}.bi-send-x::before{content:"\f6bf"}.bi-send::before{content:"\f6c0"}.bi-steam::before{content:"\f6c1"}.bi-terminal-dash::before{content:"\f6c3"}.bi-terminal-plus::before{content:"\f6c4"}.bi-terminal-split::before{content:"\f6c5"}.bi-ticket-detailed-fill::before{content:"\f6c6"}.bi-ticket-detailed::before{content:"\f6c7"}.bi-ticket-fill::before{content:"\f6c8"}.bi-ticket-perforated-fill::before{content:"\f6c9"}.bi-ticket-perforated::before{content:"\f6ca"}.bi-ticket::before{content:"\f6cb"}.bi-tiktok::before{content:"\f6cc"}.bi-window-dash::before{content:"\f6cd"}.bi-window-desktop::before{content:"\f6ce"}.bi-window-fullscreen::before{content:"\f6cf"}.bi-window-plus::before{content:"\f6d0"}.bi-window-split::before{content:"\f6d1"}.bi-window-stack::before{content:"\f6d2"}.bi-window-x::before{content:"\f6d3"}.bi-xbox::before{content:"\f6d4"}.bi-ethernet::before{content:"\f6d5"}.bi-hdmi-fill::before{content:"\f6d6"}.bi-hdmi::before{content:"\f6d7"}.bi-usb-c-fill::before{content:"\f6d8"}.bi-usb-c::before{content:"\f6d9"}.bi-usb-fill::before{content:"\f6da"}.bi-usb-plug-fill::before{content:"\f6db"}.bi-usb-plug::before{content:"\f6dc"}.bi-usb-symbol::before{content:"\f6dd"}.bi-usb::before{content:"\f6de"}.bi-boombox-fill::before{content:"\f6df"}.bi-displayport::before{content:"\f6e1"}.bi-gpu-card::before{content:"\f6e2"}.bi-memory::before{content:"\f6e3"}.bi-modem-fill::before{content:"\f6e4"}.bi-modem::before{content:"\f6e5"}.bi-motherboard-fill::before{content:"\f6e6"}.bi-motherboard::before{content:"\f6e7"}.bi-optical-audio-fill::before{content:"\f6e8"}.bi-optical-audio::before{content:"\f6e9"}.bi-pci-card::before{content:"\f6ea"}.bi-router-fill::before{content:"\f6eb"}.bi-router::before{content:"\f6ec"}.bi-thunderbolt-fill::before{content:"\f6ef"}.bi-thunderbolt::before{content:"\f6f0"}.bi-usb-drive-fill::before{content:"\f6f1"}.bi-usb-drive::before{content:"\f6f2"}.bi-usb-micro-fill::before{content:"\f6f3"}.bi-usb-micro::before{content:"\f6f4"}.bi-usb-mini-fill::before{content:"\f6f5"}.bi-usb-mini::before{content:"\f6f6"}.bi-cloud-haze2::before{content:"\f6f7"}.bi-device-hdd-fill::before{content:"\f6f8"}.bi-device-hdd::before{content:"\f6f9"}.bi-device-ssd-fill::before{content:"\f6fa"}.bi-device-ssd::before{content:"\f6fb"}.bi-displayport-fill::before{content:"\f6fc"}.bi-mortarboard-fill::before{content:"\f6fd"}.bi-mortarboard::before{content:"\f6fe"}.bi-terminal-x::before{content:"\f6ff"}.bi-arrow-through-heart-fill::before{content:"\f700"}.bi-arrow-through-heart::before{content:"\f701"}.bi-badge-sd-fill::before{content:"\f702"}.bi-badge-sd::before{content:"\f703"}.bi-bag-heart-fill::before{content:"\f704"}.bi-bag-heart::before{content:"\f705"}.bi-balloon-fill::before{content:"\f706"}.bi-balloon-heart-fill::before{content:"\f707"}.bi-balloon-heart::before{content:"\f708"}.bi-balloon::before{content:"\f709"}.bi-box2-fill::before{content:"\f70a"}.bi-box2-heart-fill::before{content:"\f70b"}.bi-box2-heart::before{content:"\f70c"}.bi-box2::before{content:"\f70d"}.bi-braces-asterisk::before{content:"\f70e"}.bi-calendar-heart-fill::before{content:"\f70f"}.bi-calendar-heart::before{content:"\f710"}.bi-calendar2-heart-fill::before{content:"\f711"}.bi-calendar2-heart::before{content:"\f712"}.bi-chat-heart-fill::before{content:"\f713"}.bi-chat-heart::before{content:"\f714"}.bi-chat-left-heart-fill::before{content:"\f715"}.bi-chat-left-heart::before{content:"\f716"}.bi-chat-right-heart-fill::before{content:"\f717"}.bi-chat-right-heart::before{content:"\f718"}.bi-chat-square-heart-fill::before{content:"\f719"}.bi-chat-square-heart::before{content:"\f71a"}.bi-clipboard-check-fill::before{content:"\f71b"}.bi-clipboard-data-fill::before{content:"\f71c"}.bi-clipboard-fill::before{content:"\f71d"}.bi-clipboard-heart-fill::before{content:"\f71e"}.bi-clipboard-heart::before{content:"\f71f"}.bi-clipboard-minus-fill::before{content:"\f720"}.bi-clipboard-plus-fill::before{content:"\f721"}.bi-clipboard-pulse::before{content:"\f722"}.bi-clipboard-x-fill::before{content:"\f723"}.bi-clipboard2-check-fill::before{content:"\f724"}.bi-clipboard2-check::before{content:"\f725"}.bi-clipboard2-data-fill::before{content:"\f726"}.bi-clipboard2-data::before{content:"\f727"}.bi-clipboard2-fill::before{content:"\f728"}.bi-clipboard2-heart-fill::before{content:"\f729"}.bi-clipboard2-heart::before{content:"\f72a"}.bi-clipboard2-minus-fill::before{content:"\f72b"}.bi-clipboard2-minus::before{content:"\f72c"}.bi-clipboard2-plus-fill::before{content:"\f72d"}.bi-clipboard2-plus::before{content:"\f72e"}.bi-clipboard2-pulse-fill::before{content:"\f72f"}.bi-clipboard2-pulse::before{content:"\f730"}.bi-clipboard2-x-fill::before{content:"\f731"}.bi-clipboard2-x::before{content:"\f732"}.bi-clipboard2::before{content:"\f733"}.bi-emoji-kiss-fill::before{content:"\f734"}.bi-emoji-kiss::before{content:"\f735"}.bi-envelope-heart-fill::before{content:"\f736"}.bi-envelope-heart::before{content:"\f737"}.bi-envelope-open-heart-fill::before{content:"\f738"}.bi-envelope-open-heart::before{content:"\f739"}.bi-envelope-paper-fill::before{content:"\f73a"}.bi-envelope-paper-heart-fill::before{content:"\f73b"}.bi-envelope-paper-heart::before{content:"\f73c"}.bi-envelope-paper::before{content:"\f73d"}.bi-filetype-aac::before{content:"\f73e"}.bi-filetype-ai::before{content:"\f73f"}.bi-filetype-bmp::before{content:"\f740"}.bi-filetype-cs::before{content:"\f741"}.bi-filetype-css::before{content:"\f742"}.bi-filetype-csv::before{content:"\f743"}.bi-filetype-doc::before{content:"\f744"}.bi-filetype-docx::before{content:"\f745"}.bi-filetype-exe::before{content:"\f746"}.bi-filetype-gif::before{content:"\f747"}.bi-filetype-heic::before{content:"\f748"}.bi-filetype-html::before{content:"\f749"}.bi-filetype-java::before{content:"\f74a"}.bi-filetype-jpg::before{content:"\f74b"}.bi-filetype-js::before{content:"\f74c"}.bi-filetype-jsx::before{content:"\f74d"}.bi-filetype-key::before{content:"\f74e"}.bi-filetype-m4p::before{content:"\f74f"}.bi-filetype-md::before{content:"\f750"}.bi-filetype-mdx::before{content:"\f751"}.bi-filetype-mov::before{content:"\f752"}.bi-filetype-mp3::before{content:"\f753"}.bi-filetype-mp4::before{content:"\f754"}.bi-filetype-otf::before{content:"\f755"}.bi-filetype-pdf::before{content:"\f756"}.bi-filetype-php::before{content:"\f757"}.bi-filetype-png::before{content:"\f758"}.bi-filetype-ppt::before{content:"\f75a"}.bi-filetype-psd::before{content:"\f75b"}.bi-filetype-py::before{content:"\f75c"}.bi-filetype-raw::before{content:"\f75d"}.bi-filetype-rb::before{content:"\f75e"}.bi-filetype-sass::before{content:"\f75f"}.bi-filetype-scss::before{content:"\f760"}.bi-filetype-sh::before{content:"\f761"}.bi-filetype-svg::before{content:"\f762"}.bi-filetype-tiff::before{content:"\f763"}.bi-filetype-tsx::before{content:"\f764"}.bi-filetype-ttf::before{content:"\f765"}.bi-filetype-txt::before{content:"\f766"}.bi-filetype-wav::before{content:"\f767"}.bi-filetype-woff::before{content:"\f768"}.bi-filetype-xls::before{content:"\f76a"}.bi-filetype-xml::before{content:"\f76b"}.bi-filetype-yml::before{content:"\f76c"}.bi-heart-arrow::before{content:"\f76d"}.bi-heart-pulse-fill::before{content:"\f76e"}.bi-heart-pulse::before{content:"\f76f"}.bi-heartbreak-fill::before{content:"\f770"}.bi-heartbreak::before{content:"\f771"}.bi-hearts::before{content:"\f772"}.bi-hospital-fill::before{content:"\f773"}.bi-hospital::before{content:"\f774"}.bi-house-heart-fill::before{content:"\f775"}.bi-house-heart::before{content:"\f776"}.bi-incognito::before{content:"\f777"}.bi-magnet-fill::before{content:"\f778"}.bi-magnet::before{content:"\f779"}.bi-person-heart::before{content:"\f77a"}.bi-person-hearts::before{content:"\f77b"}.bi-phone-flip::before{content:"\f77c"}.bi-plugin::before{content:"\f77d"}.bi-postage-fill::before{content:"\f77e"}.bi-postage-heart-fill::before{content:"\f77f"}.bi-postage-heart::before{content:"\f780"}.bi-postage::before{content:"\f781"}.bi-postcard-fill::before{content:"\f782"}.bi-postcard-heart-fill::before{content:"\f783"}.bi-postcard-heart::before{content:"\f784"}.bi-postcard::before{content:"\f785"}.bi-search-heart-fill::before{content:"\f786"}.bi-search-heart::before{content:"\f787"}.bi-sliders2-vertical::before{content:"\f788"}.bi-sliders2::before{content:"\f789"}.bi-trash3-fill::before{content:"\f78a"}.bi-trash3::before{content:"\f78b"}.bi-valentine::before{content:"\f78c"}.bi-valentine2::before{content:"\f78d"}.bi-wrench-adjustable-circle-fill::before{content:"\f78e"}.bi-wrench-adjustable-circle::before{content:"\f78f"}.bi-wrench-adjustable::before{content:"\f790"}.bi-filetype-json::before{content:"\f791"}.bi-filetype-pptx::before{content:"\f792"}.bi-filetype-xlsx::before{content:"\f793"}.bi-1-circle-fill::before{content:"\f796"}.bi-1-circle::before{content:"\f797"}.bi-1-square-fill::before{content:"\f798"}.bi-1-square::before{content:"\f799"}.bi-2-circle-fill::before{content:"\f79c"}.bi-2-circle::before{content:"\f79d"}.bi-2-square-fill::before{content:"\f79e"}.bi-2-square::before{content:"\f79f"}.bi-3-circle-fill::before{content:"\f7a2"}.bi-3-circle::before{content:"\f7a3"}.bi-3-square-fill::before{content:"\f7a4"}.bi-3-square::before{content:"\f7a5"}.bi-4-circle-fill::before{content:"\f7a8"}.bi-4-circle::before{content:"\f7a9"}.bi-4-square-fill::before{content:"\f7aa"}.bi-4-square::before{content:"\f7ab"}.bi-5-circle-fill::before{content:"\f7ae"}.bi-5-circle::before{content:"\f7af"}.bi-5-square-fill::before{content:"\f7b0"}.bi-5-square::before{content:"\f7b1"}.bi-6-circle-fill::before{content:"\f7b4"}.bi-6-circle::before{content:"\f7b5"}.bi-6-square-fill::before{content:"\f7b6"}.bi-6-square::before{content:"\f7b7"}.bi-7-circle-fill::before{content:"\f7ba"}.bi-7-circle::before{content:"\f7bb"}.bi-7-square-fill::before{content:"\f7bc"}.bi-7-square::before{content:"\f7bd"}.bi-8-circle-fill::before{content:"\f7c0"}.bi-8-circle::before{content:"\f7c1"}.bi-8-square-fill::before{content:"\f7c2"}.bi-8-square::before{content:"\f7c3"}.bi-9-circle-fill::before{content:"\f7c6"}.bi-9-circle::before{content:"\f7c7"}.bi-9-square-fill::before{content:"\f7c8"}.bi-9-square::before{content:"\f7c9"}.bi-airplane-engines-fill::before{content:"\f7ca"}.bi-airplane-engines::before{content:"\f7cb"}.bi-airplane-fill::before{content:"\f7cc"}.bi-airplane::before{content:"\f7cd"}.bi-alexa::before{content:"\f7ce"}.bi-alipay::before{content:"\f7cf"}.bi-android::before{content:"\f7d0"}.bi-android2::before{content:"\f7d1"}.bi-box-fill::before{content:"\f7d2"}.bi-box-seam-fill::before{content:"\f7d3"}.bi-browser-chrome::before{content:"\f7d4"}.bi-browser-edge::before{content:"\f7d5"}.bi-browser-firefox::before{content:"\f7d6"}.bi-browser-safari::before{content:"\f7d7"}.bi-c-circle-fill::before{content:"\f7da"}.bi-c-circle::before{content:"\f7db"}.bi-c-square-fill::before{content:"\f7dc"}.bi-c-square::before{content:"\f7dd"}.bi-capsule-pill::before{content:"\f7de"}.bi-capsule::before{content:"\f7df"}.bi-car-front-fill::before{content:"\f7e0"}.bi-car-front::before{content:"\f7e1"}.bi-cassette-fill::before{content:"\f7e2"}.bi-cassette::before{content:"\f7e3"}.bi-cc-circle-fill::before{content:"\f7e6"}.bi-cc-circle::before{content:"\f7e7"}.bi-cc-square-fill::before{content:"\f7e8"}.bi-cc-square::before{content:"\f7e9"}.bi-cup-hot-fill::before{content:"\f7ea"}.bi-cup-hot::before{content:"\f7eb"}.bi-currency-rupee::before{content:"\f7ec"}.bi-dropbox::before{content:"\f7ed"}.bi-escape::before{content:"\f7ee"}.bi-fast-forward-btn-fill::before{content:"\f7ef"}.bi-fast-forward-btn::before{content:"\f7f0"}.bi-fast-forward-circle-fill::before{content:"\f7f1"}.bi-fast-forward-circle::before{content:"\f7f2"}.bi-fast-forward-fill::before{content:"\f7f3"}.bi-fast-forward::before{content:"\f7f4"}.bi-filetype-sql::before{content:"\f7f5"}.bi-fire::before{content:"\f7f6"}.bi-google-play::before{content:"\f7f7"}.bi-h-circle-fill::before{content:"\f7fa"}.bi-h-circle::before{content:"\f7fb"}.bi-h-square-fill::before{content:"\f7fc"}.bi-h-square::before{content:"\f7fd"}.bi-indent::before{content:"\f7fe"}.bi-lungs-fill::before{content:"\f7ff"}.bi-lungs::before{content:"\f800"}.bi-microsoft-teams::before{content:"\f801"}.bi-p-circle-fill::before{content:"\f804"}.bi-p-circle::before{content:"\f805"}.bi-p-square-fill::before{content:"\f806"}.bi-p-square::before{content:"\f807"}.bi-pass-fill::before{content:"\f808"}.bi-pass::before{content:"\f809"}.bi-prescription::before{content:"\f80a"}.bi-prescription2::before{content:"\f80b"}.bi-r-circle-fill::before{content:"\f80e"}.bi-r-circle::before{content:"\f80f"}.bi-r-square-fill::before{content:"\f810"}.bi-r-square::before{content:"\f811"}.bi-repeat-1::before{content:"\f812"}.bi-repeat::before{content:"\f813"}.bi-rewind-btn-fill::before{content:"\f814"}.bi-rewind-btn::before{content:"\f815"}.bi-rewind-circle-fill::before{content:"\f816"}.bi-rewind-circle::before{content:"\f817"}.bi-rewind-fill::before{content:"\f818"}.bi-rewind::before{content:"\f819"}.bi-train-freight-front-fill::before{content:"\f81a"}.bi-train-freight-front::before{content:"\f81b"}.bi-train-front-fill::before{content:"\f81c"}.bi-train-front::before{content:"\f81d"}.bi-train-lightrail-front-fill::before{content:"\f81e"}.bi-train-lightrail-front::before{content:"\f81f"}.bi-truck-front-fill::before{content:"\f820"}.bi-truck-front::before{content:"\f821"}.bi-ubuntu::before{content:"\f822"}.bi-unindent::before{content:"\f823"}.bi-unity::before{content:"\f824"}.bi-universal-access-circle::before{content:"\f825"}.bi-universal-access::before{content:"\f826"}.bi-virus::before{content:"\f827"}.bi-virus2::before{content:"\f828"}.bi-wechat::before{content:"\f829"}.bi-yelp::before{content:"\f82a"}.bi-sign-stop-fill::before{content:"\f82b"}.bi-sign-stop-lights-fill::before{content:"\f82c"}.bi-sign-stop-lights::before{content:"\f82d"}.bi-sign-stop::before{content:"\f82e"}.bi-sign-turn-left-fill::before{content:"\f82f"}.bi-sign-turn-left::before{content:"\f830"}.bi-sign-turn-right-fill::before{content:"\f831"}.bi-sign-turn-right::before{content:"\f832"}.bi-sign-turn-slight-left-fill::before{content:"\f833"}.bi-sign-turn-slight-left::before{content:"\f834"}.bi-sign-turn-slight-right-fill::before{content:"\f835"}.bi-sign-turn-slight-right::before{content:"\f836"}.bi-sign-yield-fill::before{content:"\f837"}.bi-sign-yield::before{content:"\f838"}.bi-ev-station-fill::before{content:"\f839"}.bi-ev-station::before{content:"\f83a"}.bi-fuel-pump-diesel-fill::before{content:"\f83b"}.bi-fuel-pump-diesel::before{content:"\f83c"}.bi-fuel-pump-fill::before{content:"\f83d"}.bi-fuel-pump::before{content:"\f83e"}.bi-0-circle-fill::before{content:"\f83f"}.bi-0-circle::before{content:"\f840"}.bi-0-square-fill::before{content:"\f841"}.bi-0-square::before{content:"\f842"}.bi-rocket-fill::before{content:"\f843"}.bi-rocket-takeoff-fill::before{content:"\f844"}.bi-rocket-takeoff::before{content:"\f845"}.bi-rocket::before{content:"\f846"}.bi-stripe::before{content:"\f847"}.bi-subscript::before{content:"\f848"}.bi-superscript::before{content:"\f849"}.bi-trello::before{content:"\f84a"}.bi-envelope-at-fill::before{content:"\f84b"}.bi-envelope-at::before{content:"\f84c"}.bi-regex::before{content:"\f84d"}.bi-text-wrap::before{content:"\f84e"}.bi-sign-dead-end-fill::before{content:"\f84f"}.bi-sign-dead-end::before{content:"\f850"}.bi-sign-do-not-enter-fill::before{content:"\f851"}.bi-sign-do-not-enter::before{content:"\f852"}.bi-sign-intersection-fill::before{content:"\f853"}.bi-sign-intersection-side-fill::before{content:"\f854"}.bi-sign-intersection-side::before{content:"\f855"}.bi-sign-intersection-t-fill::before{content:"\f856"}.bi-sign-intersection-t::before{content:"\f857"}.bi-sign-intersection-y-fill::before{content:"\f858"}.bi-sign-intersection-y::before{content:"\f859"}.bi-sign-intersection::before{content:"\f85a"}.bi-sign-merge-left-fill::before{content:"\f85b"}.bi-sign-merge-left::before{content:"\f85c"}.bi-sign-merge-right-fill::before{content:"\f85d"}.bi-sign-merge-right::before{content:"\f85e"}.bi-sign-no-left-turn-fill::before{content:"\f85f"}.bi-sign-no-left-turn::before{content:"\f860"}.bi-sign-no-parking-fill::before{content:"\f861"}.bi-sign-no-parking::before{content:"\f862"}.bi-sign-no-right-turn-fill::before{content:"\f863"}.bi-sign-no-right-turn::before{content:"\f864"}.bi-sign-railroad-fill::before{content:"\f865"}.bi-sign-railroad::before{content:"\f866"}.bi-building-add::before{content:"\f867"}.bi-building-check::before{content:"\f868"}.bi-building-dash::before{content:"\f869"}.bi-building-down::before{content:"\f86a"}.bi-building-exclamation::before{content:"\f86b"}.bi-building-fill-add::before{content:"\f86c"}.bi-building-fill-check::before{content:"\f86d"}.bi-building-fill-dash::before{content:"\f86e"}.bi-building-fill-down::before{content:"\f86f"}.bi-building-fill-exclamation::before{content:"\f870"}.bi-building-fill-gear::before{content:"\f871"}.bi-building-fill-lock::before{content:"\f872"}.bi-building-fill-slash::before{content:"\f873"}.bi-building-fill-up::before{content:"\f874"}.bi-building-fill-x::before{content:"\f875"}.bi-building-fill::before{content:"\f876"}.bi-building-gear::before{content:"\f877"}.bi-building-lock::before{content:"\f878"}.bi-building-slash::before{content:"\f879"}.bi-building-up::before{content:"\f87a"}.bi-building-x::before{content:"\f87b"}.bi-buildings-fill::before{content:"\f87c"}.bi-buildings::before{content:"\f87d"}.bi-bus-front-fill::before{content:"\f87e"}.bi-bus-front::before{content:"\f87f"}.bi-ev-front-fill::before{content:"\f880"}.bi-ev-front::before{content:"\f881"}.bi-globe-americas::before{content:"\f882"}.bi-globe-asia-australia::before{content:"\f883"}.bi-globe-central-south-asia::before{content:"\f884"}.bi-globe-europe-africa::before{content:"\f885"}.bi-house-add-fill::before{content:"\f886"}.bi-house-add::before{content:"\f887"}.bi-house-check-fill::before{content:"\f888"}.bi-house-check::before{content:"\f889"}.bi-house-dash-fill::before{content:"\f88a"}.bi-house-dash::before{content:"\f88b"}.bi-house-down-fill::before{content:"\f88c"}.bi-house-down::before{content:"\f88d"}.bi-house-exclamation-fill::before{content:"\f88e"}.bi-house-exclamation::before{content:"\f88f"}.bi-house-gear-fill::before{content:"\f890"}.bi-house-gear::before{content:"\f891"}.bi-house-lock-fill::before{content:"\f892"}.bi-house-lock::before{content:"\f893"}.bi-house-slash-fill::before{content:"\f894"}.bi-house-slash::before{content:"\f895"}.bi-house-up-fill::before{content:"\f896"}.bi-house-up::before{content:"\f897"}.bi-house-x-fill::before{content:"\f898"}.bi-house-x::before{content:"\f899"}.bi-person-add::before{content:"\f89a"}.bi-person-down::before{content:"\f89b"}.bi-person-exclamation::before{content:"\f89c"}.bi-person-fill-add::before{content:"\f89d"}.bi-person-fill-check::before{content:"\f89e"}.bi-person-fill-dash::before{content:"\f89f"}.bi-person-fill-down::before{content:"\f8a0"}.bi-person-fill-exclamation::before{content:"\f8a1"}.bi-person-fill-gear::before{content:"\f8a2"}.bi-person-fill-lock::before{content:"\f8a3"}.bi-person-fill-slash::before{content:"\f8a4"}.bi-person-fill-up::before{content:"\f8a5"}.bi-person-fill-x::before{content:"\f8a6"}.bi-person-gear::before{content:"\f8a7"}.bi-person-lock::before{content:"\f8a8"}.bi-person-slash::before{content:"\f8a9"}.bi-person-up::before{content:"\f8aa"}.bi-scooter::before{content:"\f8ab"}.bi-taxi-front-fill::before{content:"\f8ac"}.bi-taxi-front::before{content:"\f8ad"}.bi-amd::before{content:"\f8ae"}.bi-database-add::before{content:"\f8af"}.bi-database-check::before{content:"\f8b0"}.bi-database-dash::before{content:"\f8b1"}.bi-database-down::before{content:"\f8b2"}.bi-database-exclamation::before{content:"\f8b3"}.bi-database-fill-add::before{content:"\f8b4"}.bi-database-fill-check::before{content:"\f8b5"}.bi-database-fill-dash::before{content:"\f8b6"}.bi-database-fill-down::before{content:"\f8b7"}.bi-database-fill-exclamation::before{content:"\f8b8"}.bi-database-fill-gear::before{content:"\f8b9"}.bi-database-fill-lock::before{content:"\f8ba"}.bi-database-fill-slash::before{content:"\f8bb"}.bi-database-fill-up::before{content:"\f8bc"}.bi-database-fill-x::before{content:"\f8bd"}.bi-database-fill::before{content:"\f8be"}.bi-database-gear::before{content:"\f8bf"}.bi-database-lock::before{content:"\f8c0"}.bi-database-slash::before{content:"\f8c1"}.bi-database-up::before{content:"\f8c2"}.bi-database-x::before{content:"\f8c3"}.bi-database::before{content:"\f8c4"}.bi-houses-fill::before{content:"\f8c5"}.bi-houses::before{content:"\f8c6"}.bi-nvidia::before{content:"\f8c7"}.bi-person-vcard-fill::before{content:"\f8c8"}.bi-person-vcard::before{content:"\f8c9"}.bi-sina-weibo::before{content:"\f8ca"}.bi-tencent-qq::before{content:"\f8cb"}.bi-wikipedia::before{content:"\f8cc"}.bi-alphabet-uppercase::before{content:"\f2a5"}.bi-alphabet::before{content:"\f68a"}.bi-amazon::before{content:"\f68d"}.bi-arrows-collapse-vertical::before{content:"\f690"}.bi-arrows-expand-vertical::before{content:"\f695"}.bi-arrows-vertical::before{content:"\f698"}.bi-arrows::before{content:"\f6a2"}.bi-ban-fill::before{content:"\f6a3"}.bi-ban::before{content:"\f6b6"}.bi-bing::before{content:"\f6c2"}.bi-cake::before{content:"\f6e0"}.bi-cake2::before{content:"\f6ed"}.bi-cookie::before{content:"\f6ee"}.bi-copy::before{content:"\f759"}.bi-crosshair::before{content:"\f769"}.bi-crosshair2::before{content:"\f794"}.bi-emoji-astonished-fill::before{content:"\f795"}.bi-emoji-astonished::before{content:"\f79a"}.bi-emoji-grimace-fill::before{content:"\f79b"}.bi-emoji-grimace::before{content:"\f7a0"}.bi-emoji-grin-fill::before{content:"\f7a1"}.bi-emoji-grin::before{content:"\f7a6"}.bi-emoji-surprise-fill::before{content:"\f7a7"}.bi-emoji-surprise::before{content:"\f7ac"}.bi-emoji-tear-fill::before{content:"\f7ad"}.bi-emoji-tear::before{content:"\f7b2"}.bi-envelope-arrow-down-fill::before{content:"\f7b3"}.bi-envelope-arrow-down::before{content:"\f7b8"}.bi-envelope-arrow-up-fill::before{content:"\f7b9"}.bi-envelope-arrow-up::before{content:"\f7be"}.bi-feather::before{content:"\f7bf"}.bi-feather2::before{content:"\f7c4"}.bi-floppy-fill::before{content:"\f7c5"}.bi-floppy::before{content:"\f7d8"}.bi-floppy2-fill::before{content:"\f7d9"}.bi-floppy2::before{content:"\f7e4"}.bi-gitlab::before{content:"\f7e5"}.bi-highlighter::before{content:"\f7f8"}.bi-marker-tip::before{content:"\f802"}.bi-nvme-fill::before{content:"\f803"}.bi-nvme::before{content:"\f80c"}.bi-opencollective::before{content:"\f80d"}.bi-pci-card-network::before{content:"\f8cd"}.bi-pci-card-sound::before{content:"\f8ce"}.bi-radar::before{content:"\f8cf"}.bi-send-arrow-down-fill::before{content:"\f8d0"}.bi-send-arrow-down::before{content:"\f8d1"}.bi-send-arrow-up-fill::before{content:"\f8d2"}.bi-send-arrow-up::before{content:"\f8d3"}.bi-sim-slash-fill::before{content:"\f8d4"}.bi-sim-slash::before{content:"\f8d5"}.bi-sourceforge::before{content:"\f8d6"}.bi-substack::before{content:"\f8d7"}.bi-threads-fill::before{content:"\f8d8"}.bi-threads::before{content:"\f8d9"}.bi-transparency::before{content:"\f8da"}.bi-twitter-x::before{content:"\f8db"}.bi-type-h4::before{content:"\f8dc"}.bi-type-h5::before{content:"\f8dd"}.bi-type-h6::before{content:"\f8de"}.bi-backpack-fill::before{content:"\f8df"}.bi-backpack::before{content:"\f8e0"}.bi-backpack2-fill::before{content:"\f8e1"}.bi-backpack2::before{content:"\f8e2"}.bi-backpack3-fill::before{content:"\f8e3"}.bi-backpack3::before{content:"\f8e4"}.bi-backpack4-fill::before{content:"\f8e5"}.bi-backpack4::before{content:"\f8e6"}.bi-brilliance::before{content:"\f8e7"}.bi-cake-fill::before{content:"\f8e8"}.bi-cake2-fill::before{content:"\f8e9"}.bi-duffle-fill::before{content:"\f8ea"}.bi-duffle::before{content:"\f8eb"}.bi-exposure::before{content:"\f8ec"}.bi-gender-neuter::before{content:"\f8ed"}.bi-highlights::before{content:"\f8ee"}.bi-luggage-fill::before{content:"\f8ef"}.bi-luggage::before{content:"\f8f0"}.bi-mailbox-flag::before{content:"\f8f1"}.bi-mailbox2-flag::before{content:"\f8f2"}.bi-noise-reduction::before{content:"\f8f3"}.bi-passport-fill::before{content:"\f8f4"}.bi-passport::before{content:"\f8f5"}.bi-person-arms-up::before{content:"\f8f6"}.bi-person-raised-hand::before{content:"\f8f7"}.bi-person-standing-dress::before{content:"\f8f8"}.bi-person-standing::before{content:"\f8f9"}.bi-person-walking::before{content:"\f8fa"}.bi-person-wheelchair::before{content:"\f8fb"}.bi-shadows::before{content:"\f8fc"}.bi-suitcase-fill::before{content:"\f8fd"}.bi-suitcase-lg-fill::before{content:"\f8fe"}.bi-suitcase-lg::before{content:"\f8ff"}.bi-suitcase::before{content:"\f900"}.bi-suitcase2-fill::before{content:"\f901"}.bi-suitcase2::before{content:"\f902"}.bi-vignette::before{content:"\f903"}.bi-bluesky::before{content:"\f7f9"}.bi-tux::before{content:"\f904"}.bi-beaker-fill::before{content:"\f905"}.bi-beaker::before{content:"\f906"}.bi-flask-fill::before{content:"\f907"}.bi-flask-florence-fill::before{content:"\f908"}.bi-flask-florence::before{content:"\f909"}.bi-flask::before{content:"\f90a"}.bi-leaf-fill::before{content:"\f90b"}.bi-leaf::before{content:"\f90c"}.bi-measuring-cup-fill::before{content:"\f90d"}.bi-measuring-cup::before{content:"\f90e"}.bi-unlock2-fill::before{content:"\f90f"}.bi-unlock2::before{content:"\f910"}.bi-battery-low::before{content:"\f911"}.bi-anthropic::before{content:"\f912"}.bi-apple-music::before{content:"\f913"}.bi-claude::before{content:"\f914"}.bi-openai::before{content:"\f915"}.bi-perplexity::before{content:"\f916"}.bi-css::before{content:"\f917"}.bi-javascript::before{content:"\f918"}.bi-typescript::before{content:"\f919"}.bi-fork-knife::before{content:"\f91a"}.bi-globe-americas-fill::before{content:"\f91b"}.bi-globe-asia-australia-fill::before{content:"\f91c"}.bi-globe-central-south-asia-fill::before{content:"\f91d"}.bi-globe-europe-africa-fill::before{content:"\f91e"} \ No newline at end of file diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.scss b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.scss new file mode 100644 index 00000000..19735c42 --- /dev/null +++ b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/bootstrap-icons.scss @@ -0,0 +1,2118 @@ +/*! + * Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/) + * Copyright 2019-2024 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE) + */ + +$bootstrap-icons-font: "bootstrap-icons" !default; +$bootstrap-icons-font-dir: "./fonts" !default; +$bootstrap-icons-font-file: "#{$bootstrap-icons-font-dir}/#{$bootstrap-icons-font}" !default; +$bootstrap-icons-font-hash: "24e3eb84d0bcaf83d77f904c78ac1f47" !default; +$bootstrap-icons-font-src: url("#{$bootstrap-icons-font-file}.woff2?#{$bootstrap-icons-font-hash}") format("woff2"), + url("#{$bootstrap-icons-font-file}.woff?#{$bootstrap-icons-font-hash}") format("woff") !default; + +@font-face { + font-display: block; + font-family: $bootstrap-icons-font; + src: $bootstrap-icons-font-src; +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: $bootstrap-icons-font !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +$bootstrap-icons-map: ( + "123": "\f67f", + "alarm-fill": "\f101", + "alarm": "\f102", + "align-bottom": "\f103", + "align-center": "\f104", + "align-end": "\f105", + "align-middle": "\f106", + "align-start": "\f107", + "align-top": "\f108", + "alt": "\f109", + "app-indicator": "\f10a", + "app": "\f10b", + "archive-fill": "\f10c", + "archive": "\f10d", + "arrow-90deg-down": "\f10e", + "arrow-90deg-left": "\f10f", + "arrow-90deg-right": "\f110", + "arrow-90deg-up": "\f111", + "arrow-bar-down": "\f112", + "arrow-bar-left": "\f113", + "arrow-bar-right": "\f114", + "arrow-bar-up": "\f115", + "arrow-clockwise": "\f116", + "arrow-counterclockwise": "\f117", + "arrow-down-circle-fill": "\f118", + "arrow-down-circle": "\f119", + "arrow-down-left-circle-fill": "\f11a", + "arrow-down-left-circle": "\f11b", + "arrow-down-left-square-fill": "\f11c", + "arrow-down-left-square": "\f11d", + "arrow-down-left": "\f11e", + "arrow-down-right-circle-fill": "\f11f", + "arrow-down-right-circle": "\f120", + "arrow-down-right-square-fill": "\f121", + "arrow-down-right-square": "\f122", + "arrow-down-right": "\f123", + "arrow-down-short": "\f124", + "arrow-down-square-fill": "\f125", + "arrow-down-square": "\f126", + "arrow-down-up": "\f127", + "arrow-down": "\f128", + "arrow-left-circle-fill": "\f129", + "arrow-left-circle": "\f12a", + "arrow-left-right": "\f12b", + "arrow-left-short": "\f12c", + "arrow-left-square-fill": "\f12d", + "arrow-left-square": "\f12e", + "arrow-left": "\f12f", + "arrow-repeat": "\f130", + "arrow-return-left": "\f131", + "arrow-return-right": "\f132", + "arrow-right-circle-fill": "\f133", + "arrow-right-circle": "\f134", + "arrow-right-short": "\f135", + "arrow-right-square-fill": "\f136", + "arrow-right-square": "\f137", + "arrow-right": "\f138", + "arrow-up-circle-fill": "\f139", + "arrow-up-circle": "\f13a", + "arrow-up-left-circle-fill": "\f13b", + "arrow-up-left-circle": "\f13c", + "arrow-up-left-square-fill": "\f13d", + "arrow-up-left-square": "\f13e", + "arrow-up-left": "\f13f", + "arrow-up-right-circle-fill": "\f140", + "arrow-up-right-circle": "\f141", + "arrow-up-right-square-fill": "\f142", + "arrow-up-right-square": "\f143", + "arrow-up-right": "\f144", + "arrow-up-short": "\f145", + "arrow-up-square-fill": "\f146", + "arrow-up-square": "\f147", + "arrow-up": "\f148", + "arrows-angle-contract": "\f149", + "arrows-angle-expand": "\f14a", + "arrows-collapse": "\f14b", + "arrows-expand": "\f14c", + "arrows-fullscreen": "\f14d", + "arrows-move": "\f14e", + "aspect-ratio-fill": "\f14f", + "aspect-ratio": "\f150", + "asterisk": "\f151", + "at": "\f152", + "award-fill": "\f153", + "award": "\f154", + "back": "\f155", + "backspace-fill": "\f156", + "backspace-reverse-fill": "\f157", + "backspace-reverse": "\f158", + "backspace": "\f159", + "badge-3d-fill": "\f15a", + "badge-3d": "\f15b", + "badge-4k-fill": "\f15c", + "badge-4k": "\f15d", + "badge-8k-fill": "\f15e", + "badge-8k": "\f15f", + "badge-ad-fill": "\f160", + "badge-ad": "\f161", + "badge-ar-fill": "\f162", + "badge-ar": "\f163", + "badge-cc-fill": "\f164", + "badge-cc": "\f165", + "badge-hd-fill": "\f166", + "badge-hd": "\f167", + "badge-tm-fill": "\f168", + "badge-tm": "\f169", + "badge-vo-fill": "\f16a", + "badge-vo": "\f16b", + "badge-vr-fill": "\f16c", + "badge-vr": "\f16d", + "badge-wc-fill": "\f16e", + "badge-wc": "\f16f", + "bag-check-fill": "\f170", + "bag-check": "\f171", + "bag-dash-fill": "\f172", + "bag-dash": "\f173", + "bag-fill": "\f174", + "bag-plus-fill": "\f175", + "bag-plus": "\f176", + "bag-x-fill": "\f177", + "bag-x": "\f178", + "bag": "\f179", + "bar-chart-fill": "\f17a", + "bar-chart-line-fill": "\f17b", + "bar-chart-line": "\f17c", + "bar-chart-steps": "\f17d", + "bar-chart": "\f17e", + "basket-fill": "\f17f", + "basket": "\f180", + "basket2-fill": "\f181", + "basket2": "\f182", + "basket3-fill": "\f183", + "basket3": "\f184", + "battery-charging": "\f185", + "battery-full": "\f186", + "battery-half": "\f187", + "battery": "\f188", + "bell-fill": "\f189", + "bell": "\f18a", + "bezier": "\f18b", + "bezier2": "\f18c", + "bicycle": "\f18d", + "binoculars-fill": "\f18e", + "binoculars": "\f18f", + "blockquote-left": "\f190", + "blockquote-right": "\f191", + "book-fill": "\f192", + "book-half": "\f193", + "book": "\f194", + "bookmark-check-fill": "\f195", + "bookmark-check": "\f196", + "bookmark-dash-fill": "\f197", + "bookmark-dash": "\f198", + "bookmark-fill": "\f199", + "bookmark-heart-fill": "\f19a", + "bookmark-heart": "\f19b", + "bookmark-plus-fill": "\f19c", + "bookmark-plus": "\f19d", + "bookmark-star-fill": "\f19e", + "bookmark-star": "\f19f", + "bookmark-x-fill": "\f1a0", + "bookmark-x": "\f1a1", + "bookmark": "\f1a2", + "bookmarks-fill": "\f1a3", + "bookmarks": "\f1a4", + "bookshelf": "\f1a5", + "bootstrap-fill": "\f1a6", + "bootstrap-reboot": "\f1a7", + "bootstrap": "\f1a8", + "border-all": "\f1a9", + "border-bottom": "\f1aa", + "border-center": "\f1ab", + "border-inner": "\f1ac", + "border-left": "\f1ad", + "border-middle": "\f1ae", + "border-outer": "\f1af", + "border-right": "\f1b0", + "border-style": "\f1b1", + "border-top": "\f1b2", + "border-width": "\f1b3", + "border": "\f1b4", + "bounding-box-circles": "\f1b5", + "bounding-box": "\f1b6", + "box-arrow-down-left": "\f1b7", + "box-arrow-down-right": "\f1b8", + "box-arrow-down": "\f1b9", + "box-arrow-in-down-left": "\f1ba", + "box-arrow-in-down-right": "\f1bb", + "box-arrow-in-down": "\f1bc", + "box-arrow-in-left": "\f1bd", + "box-arrow-in-right": "\f1be", + "box-arrow-in-up-left": "\f1bf", + "box-arrow-in-up-right": "\f1c0", + "box-arrow-in-up": "\f1c1", + "box-arrow-left": "\f1c2", + "box-arrow-right": "\f1c3", + "box-arrow-up-left": "\f1c4", + "box-arrow-up-right": "\f1c5", + "box-arrow-up": "\f1c6", + "box-seam": "\f1c7", + "box": "\f1c8", + "braces": "\f1c9", + "bricks": "\f1ca", + "briefcase-fill": "\f1cb", + "briefcase": "\f1cc", + "brightness-alt-high-fill": "\f1cd", + "brightness-alt-high": "\f1ce", + "brightness-alt-low-fill": "\f1cf", + "brightness-alt-low": "\f1d0", + "brightness-high-fill": "\f1d1", + "brightness-high": "\f1d2", + "brightness-low-fill": "\f1d3", + "brightness-low": "\f1d4", + "broadcast-pin": "\f1d5", + "broadcast": "\f1d6", + "brush-fill": "\f1d7", + "brush": "\f1d8", + "bucket-fill": "\f1d9", + "bucket": "\f1da", + "bug-fill": "\f1db", + "bug": "\f1dc", + "building": "\f1dd", + "bullseye": "\f1de", + "calculator-fill": "\f1df", + "calculator": "\f1e0", + "calendar-check-fill": "\f1e1", + "calendar-check": "\f1e2", + "calendar-date-fill": "\f1e3", + "calendar-date": "\f1e4", + "calendar-day-fill": "\f1e5", + "calendar-day": "\f1e6", + "calendar-event-fill": "\f1e7", + "calendar-event": "\f1e8", + "calendar-fill": "\f1e9", + "calendar-minus-fill": "\f1ea", + "calendar-minus": "\f1eb", + "calendar-month-fill": "\f1ec", + "calendar-month": "\f1ed", + "calendar-plus-fill": "\f1ee", + "calendar-plus": "\f1ef", + "calendar-range-fill": "\f1f0", + "calendar-range": "\f1f1", + "calendar-week-fill": "\f1f2", + "calendar-week": "\f1f3", + "calendar-x-fill": "\f1f4", + "calendar-x": "\f1f5", + "calendar": "\f1f6", + "calendar2-check-fill": "\f1f7", + "calendar2-check": "\f1f8", + "calendar2-date-fill": "\f1f9", + "calendar2-date": "\f1fa", + "calendar2-day-fill": "\f1fb", + "calendar2-day": "\f1fc", + "calendar2-event-fill": "\f1fd", + "calendar2-event": "\f1fe", + "calendar2-fill": "\f1ff", + "calendar2-minus-fill": "\f200", + "calendar2-minus": "\f201", + "calendar2-month-fill": "\f202", + "calendar2-month": "\f203", + "calendar2-plus-fill": "\f204", + "calendar2-plus": "\f205", + "calendar2-range-fill": "\f206", + "calendar2-range": "\f207", + "calendar2-week-fill": "\f208", + "calendar2-week": "\f209", + "calendar2-x-fill": "\f20a", + "calendar2-x": "\f20b", + "calendar2": "\f20c", + "calendar3-event-fill": "\f20d", + "calendar3-event": "\f20e", + "calendar3-fill": "\f20f", + "calendar3-range-fill": "\f210", + "calendar3-range": "\f211", + "calendar3-week-fill": "\f212", + "calendar3-week": "\f213", + "calendar3": "\f214", + "calendar4-event": "\f215", + "calendar4-range": "\f216", + "calendar4-week": "\f217", + "calendar4": "\f218", + "camera-fill": "\f219", + "camera-reels-fill": "\f21a", + "camera-reels": "\f21b", + "camera-video-fill": "\f21c", + "camera-video-off-fill": "\f21d", + "camera-video-off": "\f21e", + "camera-video": "\f21f", + "camera": "\f220", + "camera2": "\f221", + "capslock-fill": "\f222", + "capslock": "\f223", + "card-checklist": "\f224", + "card-heading": "\f225", + "card-image": "\f226", + "card-list": "\f227", + "card-text": "\f228", + "caret-down-fill": "\f229", + "caret-down-square-fill": "\f22a", + "caret-down-square": "\f22b", + "caret-down": "\f22c", + "caret-left-fill": "\f22d", + "caret-left-square-fill": "\f22e", + "caret-left-square": "\f22f", + "caret-left": "\f230", + "caret-right-fill": "\f231", + "caret-right-square-fill": "\f232", + "caret-right-square": "\f233", + "caret-right": "\f234", + "caret-up-fill": "\f235", + "caret-up-square-fill": "\f236", + "caret-up-square": "\f237", + "caret-up": "\f238", + "cart-check-fill": "\f239", + "cart-check": "\f23a", + "cart-dash-fill": "\f23b", + "cart-dash": "\f23c", + "cart-fill": "\f23d", + "cart-plus-fill": "\f23e", + "cart-plus": "\f23f", + "cart-x-fill": "\f240", + "cart-x": "\f241", + "cart": "\f242", + "cart2": "\f243", + "cart3": "\f244", + "cart4": "\f245", + "cash-stack": "\f246", + "cash": "\f247", + "cast": "\f248", + "chat-dots-fill": "\f249", + "chat-dots": "\f24a", + "chat-fill": "\f24b", + "chat-left-dots-fill": "\f24c", + "chat-left-dots": "\f24d", + "chat-left-fill": "\f24e", + "chat-left-quote-fill": "\f24f", + "chat-left-quote": "\f250", + "chat-left-text-fill": "\f251", + "chat-left-text": "\f252", + "chat-left": "\f253", + "chat-quote-fill": "\f254", + "chat-quote": "\f255", + "chat-right-dots-fill": "\f256", + "chat-right-dots": "\f257", + "chat-right-fill": "\f258", + "chat-right-quote-fill": "\f259", + "chat-right-quote": "\f25a", + "chat-right-text-fill": "\f25b", + "chat-right-text": "\f25c", + "chat-right": "\f25d", + "chat-square-dots-fill": "\f25e", + "chat-square-dots": "\f25f", + "chat-square-fill": "\f260", + "chat-square-quote-fill": "\f261", + "chat-square-quote": "\f262", + "chat-square-text-fill": "\f263", + "chat-square-text": "\f264", + "chat-square": "\f265", + "chat-text-fill": "\f266", + "chat-text": "\f267", + "chat": "\f268", + "check-all": "\f269", + "check-circle-fill": "\f26a", + "check-circle": "\f26b", + "check-square-fill": "\f26c", + "check-square": "\f26d", + "check": "\f26e", + "check2-all": "\f26f", + "check2-circle": "\f270", + "check2-square": "\f271", + "check2": "\f272", + "chevron-bar-contract": "\f273", + "chevron-bar-down": "\f274", + "chevron-bar-expand": "\f275", + "chevron-bar-left": "\f276", + "chevron-bar-right": "\f277", + "chevron-bar-up": "\f278", + "chevron-compact-down": "\f279", + "chevron-compact-left": "\f27a", + "chevron-compact-right": "\f27b", + "chevron-compact-up": "\f27c", + "chevron-contract": "\f27d", + "chevron-double-down": "\f27e", + "chevron-double-left": "\f27f", + "chevron-double-right": "\f280", + "chevron-double-up": "\f281", + "chevron-down": "\f282", + "chevron-expand": "\f283", + "chevron-left": "\f284", + "chevron-right": "\f285", + "chevron-up": "\f286", + "circle-fill": "\f287", + "circle-half": "\f288", + "circle-square": "\f289", + "circle": "\f28a", + "clipboard-check": "\f28b", + "clipboard-data": "\f28c", + "clipboard-minus": "\f28d", + "clipboard-plus": "\f28e", + "clipboard-x": "\f28f", + "clipboard": "\f290", + "clock-fill": "\f291", + "clock-history": "\f292", + "clock": "\f293", + "cloud-arrow-down-fill": "\f294", + "cloud-arrow-down": "\f295", + "cloud-arrow-up-fill": "\f296", + "cloud-arrow-up": "\f297", + "cloud-check-fill": "\f298", + "cloud-check": "\f299", + "cloud-download-fill": "\f29a", + "cloud-download": "\f29b", + "cloud-drizzle-fill": "\f29c", + "cloud-drizzle": "\f29d", + "cloud-fill": "\f29e", + "cloud-fog-fill": "\f29f", + "cloud-fog": "\f2a0", + "cloud-fog2-fill": "\f2a1", + "cloud-fog2": "\f2a2", + "cloud-hail-fill": "\f2a3", + "cloud-hail": "\f2a4", + "cloud-haze-fill": "\f2a6", + "cloud-haze": "\f2a7", + "cloud-haze2-fill": "\f2a8", + "cloud-lightning-fill": "\f2a9", + "cloud-lightning-rain-fill": "\f2aa", + "cloud-lightning-rain": "\f2ab", + "cloud-lightning": "\f2ac", + "cloud-minus-fill": "\f2ad", + "cloud-minus": "\f2ae", + "cloud-moon-fill": "\f2af", + "cloud-moon": "\f2b0", + "cloud-plus-fill": "\f2b1", + "cloud-plus": "\f2b2", + "cloud-rain-fill": "\f2b3", + "cloud-rain-heavy-fill": "\f2b4", + "cloud-rain-heavy": "\f2b5", + "cloud-rain": "\f2b6", + "cloud-slash-fill": "\f2b7", + "cloud-slash": "\f2b8", + "cloud-sleet-fill": "\f2b9", + "cloud-sleet": "\f2ba", + "cloud-snow-fill": "\f2bb", + "cloud-snow": "\f2bc", + "cloud-sun-fill": "\f2bd", + "cloud-sun": "\f2be", + "cloud-upload-fill": "\f2bf", + "cloud-upload": "\f2c0", + "cloud": "\f2c1", + "clouds-fill": "\f2c2", + "clouds": "\f2c3", + "cloudy-fill": "\f2c4", + "cloudy": "\f2c5", + "code-slash": "\f2c6", + "code-square": "\f2c7", + "code": "\f2c8", + "collection-fill": "\f2c9", + "collection-play-fill": "\f2ca", + "collection-play": "\f2cb", + "collection": "\f2cc", + "columns-gap": "\f2cd", + "columns": "\f2ce", + "command": "\f2cf", + "compass-fill": "\f2d0", + "compass": "\f2d1", + "cone-striped": "\f2d2", + "cone": "\f2d3", + "controller": "\f2d4", + "cpu-fill": "\f2d5", + "cpu": "\f2d6", + "credit-card-2-back-fill": "\f2d7", + "credit-card-2-back": "\f2d8", + "credit-card-2-front-fill": "\f2d9", + "credit-card-2-front": "\f2da", + "credit-card-fill": "\f2db", + "credit-card": "\f2dc", + "crop": "\f2dd", + "cup-fill": "\f2de", + "cup-straw": "\f2df", + "cup": "\f2e0", + "cursor-fill": "\f2e1", + "cursor-text": "\f2e2", + "cursor": "\f2e3", + "dash-circle-dotted": "\f2e4", + "dash-circle-fill": "\f2e5", + "dash-circle": "\f2e6", + "dash-square-dotted": "\f2e7", + "dash-square-fill": "\f2e8", + "dash-square": "\f2e9", + "dash": "\f2ea", + "diagram-2-fill": "\f2eb", + "diagram-2": "\f2ec", + "diagram-3-fill": "\f2ed", + "diagram-3": "\f2ee", + "diamond-fill": "\f2ef", + "diamond-half": "\f2f0", + "diamond": "\f2f1", + "dice-1-fill": "\f2f2", + "dice-1": "\f2f3", + "dice-2-fill": "\f2f4", + "dice-2": "\f2f5", + "dice-3-fill": "\f2f6", + "dice-3": "\f2f7", + "dice-4-fill": "\f2f8", + "dice-4": "\f2f9", + "dice-5-fill": "\f2fa", + "dice-5": "\f2fb", + "dice-6-fill": "\f2fc", + "dice-6": "\f2fd", + "disc-fill": "\f2fe", + "disc": "\f2ff", + "discord": "\f300", + "display-fill": "\f301", + "display": "\f302", + "distribute-horizontal": "\f303", + "distribute-vertical": "\f304", + "door-closed-fill": "\f305", + "door-closed": "\f306", + "door-open-fill": "\f307", + "door-open": "\f308", + "dot": "\f309", + "download": "\f30a", + "droplet-fill": "\f30b", + "droplet-half": "\f30c", + "droplet": "\f30d", + "earbuds": "\f30e", + "easel-fill": "\f30f", + "easel": "\f310", + "egg-fill": "\f311", + "egg-fried": "\f312", + "egg": "\f313", + "eject-fill": "\f314", + "eject": "\f315", + "emoji-angry-fill": "\f316", + "emoji-angry": "\f317", + "emoji-dizzy-fill": "\f318", + "emoji-dizzy": "\f319", + "emoji-expressionless-fill": "\f31a", + "emoji-expressionless": "\f31b", + "emoji-frown-fill": "\f31c", + "emoji-frown": "\f31d", + "emoji-heart-eyes-fill": "\f31e", + "emoji-heart-eyes": "\f31f", + "emoji-laughing-fill": "\f320", + "emoji-laughing": "\f321", + "emoji-neutral-fill": "\f322", + "emoji-neutral": "\f323", + "emoji-smile-fill": "\f324", + "emoji-smile-upside-down-fill": "\f325", + "emoji-smile-upside-down": "\f326", + "emoji-smile": "\f327", + "emoji-sunglasses-fill": "\f328", + "emoji-sunglasses": "\f329", + "emoji-wink-fill": "\f32a", + "emoji-wink": "\f32b", + "envelope-fill": "\f32c", + "envelope-open-fill": "\f32d", + "envelope-open": "\f32e", + "envelope": "\f32f", + "eraser-fill": "\f330", + "eraser": "\f331", + "exclamation-circle-fill": "\f332", + "exclamation-circle": "\f333", + "exclamation-diamond-fill": "\f334", + "exclamation-diamond": "\f335", + "exclamation-octagon-fill": "\f336", + "exclamation-octagon": "\f337", + "exclamation-square-fill": "\f338", + "exclamation-square": "\f339", + "exclamation-triangle-fill": "\f33a", + "exclamation-triangle": "\f33b", + "exclamation": "\f33c", + "exclude": "\f33d", + "eye-fill": "\f33e", + "eye-slash-fill": "\f33f", + "eye-slash": "\f340", + "eye": "\f341", + "eyedropper": "\f342", + "eyeglasses": "\f343", + "facebook": "\f344", + "file-arrow-down-fill": "\f345", + "file-arrow-down": "\f346", + "file-arrow-up-fill": "\f347", + "file-arrow-up": "\f348", + "file-bar-graph-fill": "\f349", + "file-bar-graph": "\f34a", + "file-binary-fill": "\f34b", + "file-binary": "\f34c", + "file-break-fill": "\f34d", + "file-break": "\f34e", + "file-check-fill": "\f34f", + "file-check": "\f350", + "file-code-fill": "\f351", + "file-code": "\f352", + "file-diff-fill": "\f353", + "file-diff": "\f354", + "file-earmark-arrow-down-fill": "\f355", + "file-earmark-arrow-down": "\f356", + "file-earmark-arrow-up-fill": "\f357", + "file-earmark-arrow-up": "\f358", + "file-earmark-bar-graph-fill": "\f359", + "file-earmark-bar-graph": "\f35a", + "file-earmark-binary-fill": "\f35b", + "file-earmark-binary": "\f35c", + "file-earmark-break-fill": "\f35d", + "file-earmark-break": "\f35e", + "file-earmark-check-fill": "\f35f", + "file-earmark-check": "\f360", + "file-earmark-code-fill": "\f361", + "file-earmark-code": "\f362", + "file-earmark-diff-fill": "\f363", + "file-earmark-diff": "\f364", + "file-earmark-easel-fill": "\f365", + "file-earmark-easel": "\f366", + "file-earmark-excel-fill": "\f367", + "file-earmark-excel": "\f368", + "file-earmark-fill": "\f369", + "file-earmark-font-fill": "\f36a", + "file-earmark-font": "\f36b", + "file-earmark-image-fill": "\f36c", + "file-earmark-image": "\f36d", + "file-earmark-lock-fill": "\f36e", + "file-earmark-lock": "\f36f", + "file-earmark-lock2-fill": "\f370", + "file-earmark-lock2": "\f371", + "file-earmark-medical-fill": "\f372", + "file-earmark-medical": "\f373", + "file-earmark-minus-fill": "\f374", + "file-earmark-minus": "\f375", + "file-earmark-music-fill": "\f376", + "file-earmark-music": "\f377", + "file-earmark-person-fill": "\f378", + "file-earmark-person": "\f379", + "file-earmark-play-fill": "\f37a", + "file-earmark-play": "\f37b", + "file-earmark-plus-fill": "\f37c", + "file-earmark-plus": "\f37d", + "file-earmark-post-fill": "\f37e", + "file-earmark-post": "\f37f", + "file-earmark-ppt-fill": "\f380", + "file-earmark-ppt": "\f381", + "file-earmark-richtext-fill": "\f382", + "file-earmark-richtext": "\f383", + "file-earmark-ruled-fill": "\f384", + "file-earmark-ruled": "\f385", + "file-earmark-slides-fill": "\f386", + "file-earmark-slides": "\f387", + "file-earmark-spreadsheet-fill": "\f388", + "file-earmark-spreadsheet": "\f389", + "file-earmark-text-fill": "\f38a", + "file-earmark-text": "\f38b", + "file-earmark-word-fill": "\f38c", + "file-earmark-word": "\f38d", + "file-earmark-x-fill": "\f38e", + "file-earmark-x": "\f38f", + "file-earmark-zip-fill": "\f390", + "file-earmark-zip": "\f391", + "file-earmark": "\f392", + "file-easel-fill": "\f393", + "file-easel": "\f394", + "file-excel-fill": "\f395", + "file-excel": "\f396", + "file-fill": "\f397", + "file-font-fill": "\f398", + "file-font": "\f399", + "file-image-fill": "\f39a", + "file-image": "\f39b", + "file-lock-fill": "\f39c", + "file-lock": "\f39d", + "file-lock2-fill": "\f39e", + "file-lock2": "\f39f", + "file-medical-fill": "\f3a0", + "file-medical": "\f3a1", + "file-minus-fill": "\f3a2", + "file-minus": "\f3a3", + "file-music-fill": "\f3a4", + "file-music": "\f3a5", + "file-person-fill": "\f3a6", + "file-person": "\f3a7", + "file-play-fill": "\f3a8", + "file-play": "\f3a9", + "file-plus-fill": "\f3aa", + "file-plus": "\f3ab", + "file-post-fill": "\f3ac", + "file-post": "\f3ad", + "file-ppt-fill": "\f3ae", + "file-ppt": "\f3af", + "file-richtext-fill": "\f3b0", + "file-richtext": "\f3b1", + "file-ruled-fill": "\f3b2", + "file-ruled": "\f3b3", + "file-slides-fill": "\f3b4", + "file-slides": "\f3b5", + "file-spreadsheet-fill": "\f3b6", + "file-spreadsheet": "\f3b7", + "file-text-fill": "\f3b8", + "file-text": "\f3b9", + "file-word-fill": "\f3ba", + "file-word": "\f3bb", + "file-x-fill": "\f3bc", + "file-x": "\f3bd", + "file-zip-fill": "\f3be", + "file-zip": "\f3bf", + "file": "\f3c0", + "files-alt": "\f3c1", + "files": "\f3c2", + "film": "\f3c3", + "filter-circle-fill": "\f3c4", + "filter-circle": "\f3c5", + "filter-left": "\f3c6", + "filter-right": "\f3c7", + "filter-square-fill": "\f3c8", + "filter-square": "\f3c9", + "filter": "\f3ca", + "flag-fill": "\f3cb", + "flag": "\f3cc", + "flower1": "\f3cd", + "flower2": "\f3ce", + "flower3": "\f3cf", + "folder-check": "\f3d0", + "folder-fill": "\f3d1", + "folder-minus": "\f3d2", + "folder-plus": "\f3d3", + "folder-symlink-fill": "\f3d4", + "folder-symlink": "\f3d5", + "folder-x": "\f3d6", + "folder": "\f3d7", + "folder2-open": "\f3d8", + "folder2": "\f3d9", + "fonts": "\f3da", + "forward-fill": "\f3db", + "forward": "\f3dc", + "front": "\f3dd", + "fullscreen-exit": "\f3de", + "fullscreen": "\f3df", + "funnel-fill": "\f3e0", + "funnel": "\f3e1", + "gear-fill": "\f3e2", + "gear-wide-connected": "\f3e3", + "gear-wide": "\f3e4", + "gear": "\f3e5", + "gem": "\f3e6", + "geo-alt-fill": "\f3e7", + "geo-alt": "\f3e8", + "geo-fill": "\f3e9", + "geo": "\f3ea", + "gift-fill": "\f3eb", + "gift": "\f3ec", + "github": "\f3ed", + "globe": "\f3ee", + "globe2": "\f3ef", + "google": "\f3f0", + "graph-down": "\f3f1", + "graph-up": "\f3f2", + "grid-1x2-fill": "\f3f3", + "grid-1x2": "\f3f4", + "grid-3x2-gap-fill": "\f3f5", + "grid-3x2-gap": "\f3f6", + "grid-3x2": "\f3f7", + "grid-3x3-gap-fill": "\f3f8", + "grid-3x3-gap": "\f3f9", + "grid-3x3": "\f3fa", + "grid-fill": "\f3fb", + "grid": "\f3fc", + "grip-horizontal": "\f3fd", + "grip-vertical": "\f3fe", + "hammer": "\f3ff", + "hand-index-fill": "\f400", + "hand-index-thumb-fill": "\f401", + "hand-index-thumb": "\f402", + "hand-index": "\f403", + "hand-thumbs-down-fill": "\f404", + "hand-thumbs-down": "\f405", + "hand-thumbs-up-fill": "\f406", + "hand-thumbs-up": "\f407", + "handbag-fill": "\f408", + "handbag": "\f409", + "hash": "\f40a", + "hdd-fill": "\f40b", + "hdd-network-fill": "\f40c", + "hdd-network": "\f40d", + "hdd-rack-fill": "\f40e", + "hdd-rack": "\f40f", + "hdd-stack-fill": "\f410", + "hdd-stack": "\f411", + "hdd": "\f412", + "headphones": "\f413", + "headset": "\f414", + "heart-fill": "\f415", + "heart-half": "\f416", + "heart": "\f417", + "heptagon-fill": "\f418", + "heptagon-half": "\f419", + "heptagon": "\f41a", + "hexagon-fill": "\f41b", + "hexagon-half": "\f41c", + "hexagon": "\f41d", + "hourglass-bottom": "\f41e", + "hourglass-split": "\f41f", + "hourglass-top": "\f420", + "hourglass": "\f421", + "house-door-fill": "\f422", + "house-door": "\f423", + "house-fill": "\f424", + "house": "\f425", + "hr": "\f426", + "hurricane": "\f427", + "image-alt": "\f428", + "image-fill": "\f429", + "image": "\f42a", + "images": "\f42b", + "inbox-fill": "\f42c", + "inbox": "\f42d", + "inboxes-fill": "\f42e", + "inboxes": "\f42f", + "info-circle-fill": "\f430", + "info-circle": "\f431", + "info-square-fill": "\f432", + "info-square": "\f433", + "info": "\f434", + "input-cursor-text": "\f435", + "input-cursor": "\f436", + "instagram": "\f437", + "intersect": "\f438", + "journal-album": "\f439", + "journal-arrow-down": "\f43a", + "journal-arrow-up": "\f43b", + "journal-bookmark-fill": "\f43c", + "journal-bookmark": "\f43d", + "journal-check": "\f43e", + "journal-code": "\f43f", + "journal-medical": "\f440", + "journal-minus": "\f441", + "journal-plus": "\f442", + "journal-richtext": "\f443", + "journal-text": "\f444", + "journal-x": "\f445", + "journal": "\f446", + "journals": "\f447", + "joystick": "\f448", + "justify-left": "\f449", + "justify-right": "\f44a", + "justify": "\f44b", + "kanban-fill": "\f44c", + "kanban": "\f44d", + "key-fill": "\f44e", + "key": "\f44f", + "keyboard-fill": "\f450", + "keyboard": "\f451", + "ladder": "\f452", + "lamp-fill": "\f453", + "lamp": "\f454", + "laptop-fill": "\f455", + "laptop": "\f456", + "layer-backward": "\f457", + "layer-forward": "\f458", + "layers-fill": "\f459", + "layers-half": "\f45a", + "layers": "\f45b", + "layout-sidebar-inset-reverse": "\f45c", + "layout-sidebar-inset": "\f45d", + "layout-sidebar-reverse": "\f45e", + "layout-sidebar": "\f45f", + "layout-split": "\f460", + "layout-text-sidebar-reverse": "\f461", + "layout-text-sidebar": "\f462", + "layout-text-window-reverse": "\f463", + "layout-text-window": "\f464", + "layout-three-columns": "\f465", + "layout-wtf": "\f466", + "life-preserver": "\f467", + "lightbulb-fill": "\f468", + "lightbulb-off-fill": "\f469", + "lightbulb-off": "\f46a", + "lightbulb": "\f46b", + "lightning-charge-fill": "\f46c", + "lightning-charge": "\f46d", + "lightning-fill": "\f46e", + "lightning": "\f46f", + "link-45deg": "\f470", + "link": "\f471", + "linkedin": "\f472", + "list-check": "\f473", + "list-nested": "\f474", + "list-ol": "\f475", + "list-stars": "\f476", + "list-task": "\f477", + "list-ul": "\f478", + "list": "\f479", + "lock-fill": "\f47a", + "lock": "\f47b", + "mailbox": "\f47c", + "mailbox2": "\f47d", + "map-fill": "\f47e", + "map": "\f47f", + "markdown-fill": "\f480", + "markdown": "\f481", + "mask": "\f482", + "megaphone-fill": "\f483", + "megaphone": "\f484", + "menu-app-fill": "\f485", + "menu-app": "\f486", + "menu-button-fill": "\f487", + "menu-button-wide-fill": "\f488", + "menu-button-wide": "\f489", + "menu-button": "\f48a", + "menu-down": "\f48b", + "menu-up": "\f48c", + "mic-fill": "\f48d", + "mic-mute-fill": "\f48e", + "mic-mute": "\f48f", + "mic": "\f490", + "minecart-loaded": "\f491", + "minecart": "\f492", + "moisture": "\f493", + "moon-fill": "\f494", + "moon-stars-fill": "\f495", + "moon-stars": "\f496", + "moon": "\f497", + "mouse-fill": "\f498", + "mouse": "\f499", + "mouse2-fill": "\f49a", + "mouse2": "\f49b", + "mouse3-fill": "\f49c", + "mouse3": "\f49d", + "music-note-beamed": "\f49e", + "music-note-list": "\f49f", + "music-note": "\f4a0", + "music-player-fill": "\f4a1", + "music-player": "\f4a2", + "newspaper": "\f4a3", + "node-minus-fill": "\f4a4", + "node-minus": "\f4a5", + "node-plus-fill": "\f4a6", + "node-plus": "\f4a7", + "nut-fill": "\f4a8", + "nut": "\f4a9", + "octagon-fill": "\f4aa", + "octagon-half": "\f4ab", + "octagon": "\f4ac", + "option": "\f4ad", + "outlet": "\f4ae", + "paint-bucket": "\f4af", + "palette-fill": "\f4b0", + "palette": "\f4b1", + "palette2": "\f4b2", + "paperclip": "\f4b3", + "paragraph": "\f4b4", + "patch-check-fill": "\f4b5", + "patch-check": "\f4b6", + "patch-exclamation-fill": "\f4b7", + "patch-exclamation": "\f4b8", + "patch-minus-fill": "\f4b9", + "patch-minus": "\f4ba", + "patch-plus-fill": "\f4bb", + "patch-plus": "\f4bc", + "patch-question-fill": "\f4bd", + "patch-question": "\f4be", + "pause-btn-fill": "\f4bf", + "pause-btn": "\f4c0", + "pause-circle-fill": "\f4c1", + "pause-circle": "\f4c2", + "pause-fill": "\f4c3", + "pause": "\f4c4", + "peace-fill": "\f4c5", + "peace": "\f4c6", + "pen-fill": "\f4c7", + "pen": "\f4c8", + "pencil-fill": "\f4c9", + "pencil-square": "\f4ca", + "pencil": "\f4cb", + "pentagon-fill": "\f4cc", + "pentagon-half": "\f4cd", + "pentagon": "\f4ce", + "people-fill": "\f4cf", + "people": "\f4d0", + "percent": "\f4d1", + "person-badge-fill": "\f4d2", + "person-badge": "\f4d3", + "person-bounding-box": "\f4d4", + "person-check-fill": "\f4d5", + "person-check": "\f4d6", + "person-circle": "\f4d7", + "person-dash-fill": "\f4d8", + "person-dash": "\f4d9", + "person-fill": "\f4da", + "person-lines-fill": "\f4db", + "person-plus-fill": "\f4dc", + "person-plus": "\f4dd", + "person-square": "\f4de", + "person-x-fill": "\f4df", + "person-x": "\f4e0", + "person": "\f4e1", + "phone-fill": "\f4e2", + "phone-landscape-fill": "\f4e3", + "phone-landscape": "\f4e4", + "phone-vibrate-fill": "\f4e5", + "phone-vibrate": "\f4e6", + "phone": "\f4e7", + "pie-chart-fill": "\f4e8", + "pie-chart": "\f4e9", + "pin-angle-fill": "\f4ea", + "pin-angle": "\f4eb", + "pin-fill": "\f4ec", + "pin": "\f4ed", + "pip-fill": "\f4ee", + "pip": "\f4ef", + "play-btn-fill": "\f4f0", + "play-btn": "\f4f1", + "play-circle-fill": "\f4f2", + "play-circle": "\f4f3", + "play-fill": "\f4f4", + "play": "\f4f5", + "plug-fill": "\f4f6", + "plug": "\f4f7", + "plus-circle-dotted": "\f4f8", + "plus-circle-fill": "\f4f9", + "plus-circle": "\f4fa", + "plus-square-dotted": "\f4fb", + "plus-square-fill": "\f4fc", + "plus-square": "\f4fd", + "plus": "\f4fe", + "power": "\f4ff", + "printer-fill": "\f500", + "printer": "\f501", + "puzzle-fill": "\f502", + "puzzle": "\f503", + "question-circle-fill": "\f504", + "question-circle": "\f505", + "question-diamond-fill": "\f506", + "question-diamond": "\f507", + "question-octagon-fill": "\f508", + "question-octagon": "\f509", + "question-square-fill": "\f50a", + "question-square": "\f50b", + "question": "\f50c", + "rainbow": "\f50d", + "receipt-cutoff": "\f50e", + "receipt": "\f50f", + "reception-0": "\f510", + "reception-1": "\f511", + "reception-2": "\f512", + "reception-3": "\f513", + "reception-4": "\f514", + "record-btn-fill": "\f515", + "record-btn": "\f516", + "record-circle-fill": "\f517", + "record-circle": "\f518", + "record-fill": "\f519", + "record": "\f51a", + "record2-fill": "\f51b", + "record2": "\f51c", + "reply-all-fill": "\f51d", + "reply-all": "\f51e", + "reply-fill": "\f51f", + "reply": "\f520", + "rss-fill": "\f521", + "rss": "\f522", + "rulers": "\f523", + "save-fill": "\f524", + "save": "\f525", + "save2-fill": "\f526", + "save2": "\f527", + "scissors": "\f528", + "screwdriver": "\f529", + "search": "\f52a", + "segmented-nav": "\f52b", + "server": "\f52c", + "share-fill": "\f52d", + "share": "\f52e", + "shield-check": "\f52f", + "shield-exclamation": "\f530", + "shield-fill-check": "\f531", + "shield-fill-exclamation": "\f532", + "shield-fill-minus": "\f533", + "shield-fill-plus": "\f534", + "shield-fill-x": "\f535", + "shield-fill": "\f536", + "shield-lock-fill": "\f537", + "shield-lock": "\f538", + "shield-minus": "\f539", + "shield-plus": "\f53a", + "shield-shaded": "\f53b", + "shield-slash-fill": "\f53c", + "shield-slash": "\f53d", + "shield-x": "\f53e", + "shield": "\f53f", + "shift-fill": "\f540", + "shift": "\f541", + "shop-window": "\f542", + "shop": "\f543", + "shuffle": "\f544", + "signpost-2-fill": "\f545", + "signpost-2": "\f546", + "signpost-fill": "\f547", + "signpost-split-fill": "\f548", + "signpost-split": "\f549", + "signpost": "\f54a", + "sim-fill": "\f54b", + "sim": "\f54c", + "skip-backward-btn-fill": "\f54d", + "skip-backward-btn": "\f54e", + "skip-backward-circle-fill": "\f54f", + "skip-backward-circle": "\f550", + "skip-backward-fill": "\f551", + "skip-backward": "\f552", + "skip-end-btn-fill": "\f553", + "skip-end-btn": "\f554", + "skip-end-circle-fill": "\f555", + "skip-end-circle": "\f556", + "skip-end-fill": "\f557", + "skip-end": "\f558", + "skip-forward-btn-fill": "\f559", + "skip-forward-btn": "\f55a", + "skip-forward-circle-fill": "\f55b", + "skip-forward-circle": "\f55c", + "skip-forward-fill": "\f55d", + "skip-forward": "\f55e", + "skip-start-btn-fill": "\f55f", + "skip-start-btn": "\f560", + "skip-start-circle-fill": "\f561", + "skip-start-circle": "\f562", + "skip-start-fill": "\f563", + "skip-start": "\f564", + "slack": "\f565", + "slash-circle-fill": "\f566", + "slash-circle": "\f567", + "slash-square-fill": "\f568", + "slash-square": "\f569", + "slash": "\f56a", + "sliders": "\f56b", + "smartwatch": "\f56c", + "snow": "\f56d", + "snow2": "\f56e", + "snow3": "\f56f", + "sort-alpha-down-alt": "\f570", + "sort-alpha-down": "\f571", + "sort-alpha-up-alt": "\f572", + "sort-alpha-up": "\f573", + "sort-down-alt": "\f574", + "sort-down": "\f575", + "sort-numeric-down-alt": "\f576", + "sort-numeric-down": "\f577", + "sort-numeric-up-alt": "\f578", + "sort-numeric-up": "\f579", + "sort-up-alt": "\f57a", + "sort-up": "\f57b", + "soundwave": "\f57c", + "speaker-fill": "\f57d", + "speaker": "\f57e", + "speedometer": "\f57f", + "speedometer2": "\f580", + "spellcheck": "\f581", + "square-fill": "\f582", + "square-half": "\f583", + "square": "\f584", + "stack": "\f585", + "star-fill": "\f586", + "star-half": "\f587", + "star": "\f588", + "stars": "\f589", + "stickies-fill": "\f58a", + "stickies": "\f58b", + "sticky-fill": "\f58c", + "sticky": "\f58d", + "stop-btn-fill": "\f58e", + "stop-btn": "\f58f", + "stop-circle-fill": "\f590", + "stop-circle": "\f591", + "stop-fill": "\f592", + "stop": "\f593", + "stoplights-fill": "\f594", + "stoplights": "\f595", + "stopwatch-fill": "\f596", + "stopwatch": "\f597", + "subtract": "\f598", + "suit-club-fill": "\f599", + "suit-club": "\f59a", + "suit-diamond-fill": "\f59b", + "suit-diamond": "\f59c", + "suit-heart-fill": "\f59d", + "suit-heart": "\f59e", + "suit-spade-fill": "\f59f", + "suit-spade": "\f5a0", + "sun-fill": "\f5a1", + "sun": "\f5a2", + "sunglasses": "\f5a3", + "sunrise-fill": "\f5a4", + "sunrise": "\f5a5", + "sunset-fill": "\f5a6", + "sunset": "\f5a7", + "symmetry-horizontal": "\f5a8", + "symmetry-vertical": "\f5a9", + "table": "\f5aa", + "tablet-fill": "\f5ab", + "tablet-landscape-fill": "\f5ac", + "tablet-landscape": "\f5ad", + "tablet": "\f5ae", + "tag-fill": "\f5af", + "tag": "\f5b0", + "tags-fill": "\f5b1", + "tags": "\f5b2", + "telegram": "\f5b3", + "telephone-fill": "\f5b4", + "telephone-forward-fill": "\f5b5", + "telephone-forward": "\f5b6", + "telephone-inbound-fill": "\f5b7", + "telephone-inbound": "\f5b8", + "telephone-minus-fill": "\f5b9", + "telephone-minus": "\f5ba", + "telephone-outbound-fill": "\f5bb", + "telephone-outbound": "\f5bc", + "telephone-plus-fill": "\f5bd", + "telephone-plus": "\f5be", + "telephone-x-fill": "\f5bf", + "telephone-x": "\f5c0", + "telephone": "\f5c1", + "terminal-fill": "\f5c2", + "terminal": "\f5c3", + "text-center": "\f5c4", + "text-indent-left": "\f5c5", + "text-indent-right": "\f5c6", + "text-left": "\f5c7", + "text-paragraph": "\f5c8", + "text-right": "\f5c9", + "textarea-resize": "\f5ca", + "textarea-t": "\f5cb", + "textarea": "\f5cc", + "thermometer-half": "\f5cd", + "thermometer-high": "\f5ce", + "thermometer-low": "\f5cf", + "thermometer-snow": "\f5d0", + "thermometer-sun": "\f5d1", + "thermometer": "\f5d2", + "three-dots-vertical": "\f5d3", + "three-dots": "\f5d4", + "toggle-off": "\f5d5", + "toggle-on": "\f5d6", + "toggle2-off": "\f5d7", + "toggle2-on": "\f5d8", + "toggles": "\f5d9", + "toggles2": "\f5da", + "tools": "\f5db", + "tornado": "\f5dc", + "trash-fill": "\f5dd", + "trash": "\f5de", + "trash2-fill": "\f5df", + "trash2": "\f5e0", + "tree-fill": "\f5e1", + "tree": "\f5e2", + "triangle-fill": "\f5e3", + "triangle-half": "\f5e4", + "triangle": "\f5e5", + "trophy-fill": "\f5e6", + "trophy": "\f5e7", + "tropical-storm": "\f5e8", + "truck-flatbed": "\f5e9", + "truck": "\f5ea", + "tsunami": "\f5eb", + "tv-fill": "\f5ec", + "tv": "\f5ed", + "twitch": "\f5ee", + "twitter": "\f5ef", + "type-bold": "\f5f0", + "type-h1": "\f5f1", + "type-h2": "\f5f2", + "type-h3": "\f5f3", + "type-italic": "\f5f4", + "type-strikethrough": "\f5f5", + "type-underline": "\f5f6", + "type": "\f5f7", + "ui-checks-grid": "\f5f8", + "ui-checks": "\f5f9", + "ui-radios-grid": "\f5fa", + "ui-radios": "\f5fb", + "umbrella-fill": "\f5fc", + "umbrella": "\f5fd", + "union": "\f5fe", + "unlock-fill": "\f5ff", + "unlock": "\f600", + "upc-scan": "\f601", + "upc": "\f602", + "upload": "\f603", + "vector-pen": "\f604", + "view-list": "\f605", + "view-stacked": "\f606", + "vinyl-fill": "\f607", + "vinyl": "\f608", + "voicemail": "\f609", + "volume-down-fill": "\f60a", + "volume-down": "\f60b", + "volume-mute-fill": "\f60c", + "volume-mute": "\f60d", + "volume-off-fill": "\f60e", + "volume-off": "\f60f", + "volume-up-fill": "\f610", + "volume-up": "\f611", + "vr": "\f612", + "wallet-fill": "\f613", + "wallet": "\f614", + "wallet2": "\f615", + "watch": "\f616", + "water": "\f617", + "whatsapp": "\f618", + "wifi-1": "\f619", + "wifi-2": "\f61a", + "wifi-off": "\f61b", + "wifi": "\f61c", + "wind": "\f61d", + "window-dock": "\f61e", + "window-sidebar": "\f61f", + "window": "\f620", + "wrench": "\f621", + "x-circle-fill": "\f622", + "x-circle": "\f623", + "x-diamond-fill": "\f624", + "x-diamond": "\f625", + "x-octagon-fill": "\f626", + "x-octagon": "\f627", + "x-square-fill": "\f628", + "x-square": "\f629", + "x": "\f62a", + "youtube": "\f62b", + "zoom-in": "\f62c", + "zoom-out": "\f62d", + "bank": "\f62e", + "bank2": "\f62f", + "bell-slash-fill": "\f630", + "bell-slash": "\f631", + "cash-coin": "\f632", + "check-lg": "\f633", + "coin": "\f634", + "currency-bitcoin": "\f635", + "currency-dollar": "\f636", + "currency-euro": "\f637", + "currency-exchange": "\f638", + "currency-pound": "\f639", + "currency-yen": "\f63a", + "dash-lg": "\f63b", + "exclamation-lg": "\f63c", + "file-earmark-pdf-fill": "\f63d", + "file-earmark-pdf": "\f63e", + "file-pdf-fill": "\f63f", + "file-pdf": "\f640", + "gender-ambiguous": "\f641", + "gender-female": "\f642", + "gender-male": "\f643", + "gender-trans": "\f644", + "headset-vr": "\f645", + "info-lg": "\f646", + "mastodon": "\f647", + "messenger": "\f648", + "piggy-bank-fill": "\f649", + "piggy-bank": "\f64a", + "pin-map-fill": "\f64b", + "pin-map": "\f64c", + "plus-lg": "\f64d", + "question-lg": "\f64e", + "recycle": "\f64f", + "reddit": "\f650", + "safe-fill": "\f651", + "safe2-fill": "\f652", + "safe2": "\f653", + "sd-card-fill": "\f654", + "sd-card": "\f655", + "skype": "\f656", + "slash-lg": "\f657", + "translate": "\f658", + "x-lg": "\f659", + "safe": "\f65a", + "apple": "\f65b", + "microsoft": "\f65d", + "windows": "\f65e", + "behance": "\f65c", + "dribbble": "\f65f", + "line": "\f660", + "medium": "\f661", + "paypal": "\f662", + "pinterest": "\f663", + "signal": "\f664", + "snapchat": "\f665", + "spotify": "\f666", + "stack-overflow": "\f667", + "strava": "\f668", + "wordpress": "\f669", + "vimeo": "\f66a", + "activity": "\f66b", + "easel2-fill": "\f66c", + "easel2": "\f66d", + "easel3-fill": "\f66e", + "easel3": "\f66f", + "fan": "\f670", + "fingerprint": "\f671", + "graph-down-arrow": "\f672", + "graph-up-arrow": "\f673", + "hypnotize": "\f674", + "magic": "\f675", + "person-rolodex": "\f676", + "person-video": "\f677", + "person-video2": "\f678", + "person-video3": "\f679", + "person-workspace": "\f67a", + "radioactive": "\f67b", + "webcam-fill": "\f67c", + "webcam": "\f67d", + "yin-yang": "\f67e", + "bandaid-fill": "\f680", + "bandaid": "\f681", + "bluetooth": "\f682", + "body-text": "\f683", + "boombox": "\f684", + "boxes": "\f685", + "dpad-fill": "\f686", + "dpad": "\f687", + "ear-fill": "\f688", + "ear": "\f689", + "envelope-check-fill": "\f68b", + "envelope-check": "\f68c", + "envelope-dash-fill": "\f68e", + "envelope-dash": "\f68f", + "envelope-exclamation-fill": "\f691", + "envelope-exclamation": "\f692", + "envelope-plus-fill": "\f693", + "envelope-plus": "\f694", + "envelope-slash-fill": "\f696", + "envelope-slash": "\f697", + "envelope-x-fill": "\f699", + "envelope-x": "\f69a", + "explicit-fill": "\f69b", + "explicit": "\f69c", + "git": "\f69d", + "infinity": "\f69e", + "list-columns-reverse": "\f69f", + "list-columns": "\f6a0", + "meta": "\f6a1", + "nintendo-switch": "\f6a4", + "pc-display-horizontal": "\f6a5", + "pc-display": "\f6a6", + "pc-horizontal": "\f6a7", + "pc": "\f6a8", + "playstation": "\f6a9", + "plus-slash-minus": "\f6aa", + "projector-fill": "\f6ab", + "projector": "\f6ac", + "qr-code-scan": "\f6ad", + "qr-code": "\f6ae", + "quora": "\f6af", + "quote": "\f6b0", + "robot": "\f6b1", + "send-check-fill": "\f6b2", + "send-check": "\f6b3", + "send-dash-fill": "\f6b4", + "send-dash": "\f6b5", + "send-exclamation-fill": "\f6b7", + "send-exclamation": "\f6b8", + "send-fill": "\f6b9", + "send-plus-fill": "\f6ba", + "send-plus": "\f6bb", + "send-slash-fill": "\f6bc", + "send-slash": "\f6bd", + "send-x-fill": "\f6be", + "send-x": "\f6bf", + "send": "\f6c0", + "steam": "\f6c1", + "terminal-dash": "\f6c3", + "terminal-plus": "\f6c4", + "terminal-split": "\f6c5", + "ticket-detailed-fill": "\f6c6", + "ticket-detailed": "\f6c7", + "ticket-fill": "\f6c8", + "ticket-perforated-fill": "\f6c9", + "ticket-perforated": "\f6ca", + "ticket": "\f6cb", + "tiktok": "\f6cc", + "window-dash": "\f6cd", + "window-desktop": "\f6ce", + "window-fullscreen": "\f6cf", + "window-plus": "\f6d0", + "window-split": "\f6d1", + "window-stack": "\f6d2", + "window-x": "\f6d3", + "xbox": "\f6d4", + "ethernet": "\f6d5", + "hdmi-fill": "\f6d6", + "hdmi": "\f6d7", + "usb-c-fill": "\f6d8", + "usb-c": "\f6d9", + "usb-fill": "\f6da", + "usb-plug-fill": "\f6db", + "usb-plug": "\f6dc", + "usb-symbol": "\f6dd", + "usb": "\f6de", + "boombox-fill": "\f6df", + "displayport": "\f6e1", + "gpu-card": "\f6e2", + "memory": "\f6e3", + "modem-fill": "\f6e4", + "modem": "\f6e5", + "motherboard-fill": "\f6e6", + "motherboard": "\f6e7", + "optical-audio-fill": "\f6e8", + "optical-audio": "\f6e9", + "pci-card": "\f6ea", + "router-fill": "\f6eb", + "router": "\f6ec", + "thunderbolt-fill": "\f6ef", + "thunderbolt": "\f6f0", + "usb-drive-fill": "\f6f1", + "usb-drive": "\f6f2", + "usb-micro-fill": "\f6f3", + "usb-micro": "\f6f4", + "usb-mini-fill": "\f6f5", + "usb-mini": "\f6f6", + "cloud-haze2": "\f6f7", + "device-hdd-fill": "\f6f8", + "device-hdd": "\f6f9", + "device-ssd-fill": "\f6fa", + "device-ssd": "\f6fb", + "displayport-fill": "\f6fc", + "mortarboard-fill": "\f6fd", + "mortarboard": "\f6fe", + "terminal-x": "\f6ff", + "arrow-through-heart-fill": "\f700", + "arrow-through-heart": "\f701", + "badge-sd-fill": "\f702", + "badge-sd": "\f703", + "bag-heart-fill": "\f704", + "bag-heart": "\f705", + "balloon-fill": "\f706", + "balloon-heart-fill": "\f707", + "balloon-heart": "\f708", + "balloon": "\f709", + "box2-fill": "\f70a", + "box2-heart-fill": "\f70b", + "box2-heart": "\f70c", + "box2": "\f70d", + "braces-asterisk": "\f70e", + "calendar-heart-fill": "\f70f", + "calendar-heart": "\f710", + "calendar2-heart-fill": "\f711", + "calendar2-heart": "\f712", + "chat-heart-fill": "\f713", + "chat-heart": "\f714", + "chat-left-heart-fill": "\f715", + "chat-left-heart": "\f716", + "chat-right-heart-fill": "\f717", + "chat-right-heart": "\f718", + "chat-square-heart-fill": "\f719", + "chat-square-heart": "\f71a", + "clipboard-check-fill": "\f71b", + "clipboard-data-fill": "\f71c", + "clipboard-fill": "\f71d", + "clipboard-heart-fill": "\f71e", + "clipboard-heart": "\f71f", + "clipboard-minus-fill": "\f720", + "clipboard-plus-fill": "\f721", + "clipboard-pulse": "\f722", + "clipboard-x-fill": "\f723", + "clipboard2-check-fill": "\f724", + "clipboard2-check": "\f725", + "clipboard2-data-fill": "\f726", + "clipboard2-data": "\f727", + "clipboard2-fill": "\f728", + "clipboard2-heart-fill": "\f729", + "clipboard2-heart": "\f72a", + "clipboard2-minus-fill": "\f72b", + "clipboard2-minus": "\f72c", + "clipboard2-plus-fill": "\f72d", + "clipboard2-plus": "\f72e", + "clipboard2-pulse-fill": "\f72f", + "clipboard2-pulse": "\f730", + "clipboard2-x-fill": "\f731", + "clipboard2-x": "\f732", + "clipboard2": "\f733", + "emoji-kiss-fill": "\f734", + "emoji-kiss": "\f735", + "envelope-heart-fill": "\f736", + "envelope-heart": "\f737", + "envelope-open-heart-fill": "\f738", + "envelope-open-heart": "\f739", + "envelope-paper-fill": "\f73a", + "envelope-paper-heart-fill": "\f73b", + "envelope-paper-heart": "\f73c", + "envelope-paper": "\f73d", + "filetype-aac": "\f73e", + "filetype-ai": "\f73f", + "filetype-bmp": "\f740", + "filetype-cs": "\f741", + "filetype-css": "\f742", + "filetype-csv": "\f743", + "filetype-doc": "\f744", + "filetype-docx": "\f745", + "filetype-exe": "\f746", + "filetype-gif": "\f747", + "filetype-heic": "\f748", + "filetype-html": "\f749", + "filetype-java": "\f74a", + "filetype-jpg": "\f74b", + "filetype-js": "\f74c", + "filetype-jsx": "\f74d", + "filetype-key": "\f74e", + "filetype-m4p": "\f74f", + "filetype-md": "\f750", + "filetype-mdx": "\f751", + "filetype-mov": "\f752", + "filetype-mp3": "\f753", + "filetype-mp4": "\f754", + "filetype-otf": "\f755", + "filetype-pdf": "\f756", + "filetype-php": "\f757", + "filetype-png": "\f758", + "filetype-ppt": "\f75a", + "filetype-psd": "\f75b", + "filetype-py": "\f75c", + "filetype-raw": "\f75d", + "filetype-rb": "\f75e", + "filetype-sass": "\f75f", + "filetype-scss": "\f760", + "filetype-sh": "\f761", + "filetype-svg": "\f762", + "filetype-tiff": "\f763", + "filetype-tsx": "\f764", + "filetype-ttf": "\f765", + "filetype-txt": "\f766", + "filetype-wav": "\f767", + "filetype-woff": "\f768", + "filetype-xls": "\f76a", + "filetype-xml": "\f76b", + "filetype-yml": "\f76c", + "heart-arrow": "\f76d", + "heart-pulse-fill": "\f76e", + "heart-pulse": "\f76f", + "heartbreak-fill": "\f770", + "heartbreak": "\f771", + "hearts": "\f772", + "hospital-fill": "\f773", + "hospital": "\f774", + "house-heart-fill": "\f775", + "house-heart": "\f776", + "incognito": "\f777", + "magnet-fill": "\f778", + "magnet": "\f779", + "person-heart": "\f77a", + "person-hearts": "\f77b", + "phone-flip": "\f77c", + "plugin": "\f77d", + "postage-fill": "\f77e", + "postage-heart-fill": "\f77f", + "postage-heart": "\f780", + "postage": "\f781", + "postcard-fill": "\f782", + "postcard-heart-fill": "\f783", + "postcard-heart": "\f784", + "postcard": "\f785", + "search-heart-fill": "\f786", + "search-heart": "\f787", + "sliders2-vertical": "\f788", + "sliders2": "\f789", + "trash3-fill": "\f78a", + "trash3": "\f78b", + "valentine": "\f78c", + "valentine2": "\f78d", + "wrench-adjustable-circle-fill": "\f78e", + "wrench-adjustable-circle": "\f78f", + "wrench-adjustable": "\f790", + "filetype-json": "\f791", + "filetype-pptx": "\f792", + "filetype-xlsx": "\f793", + "1-circle-fill": "\f796", + "1-circle": "\f797", + "1-square-fill": "\f798", + "1-square": "\f799", + "2-circle-fill": "\f79c", + "2-circle": "\f79d", + "2-square-fill": "\f79e", + "2-square": "\f79f", + "3-circle-fill": "\f7a2", + "3-circle": "\f7a3", + "3-square-fill": "\f7a4", + "3-square": "\f7a5", + "4-circle-fill": "\f7a8", + "4-circle": "\f7a9", + "4-square-fill": "\f7aa", + "4-square": "\f7ab", + "5-circle-fill": "\f7ae", + "5-circle": "\f7af", + "5-square-fill": "\f7b0", + "5-square": "\f7b1", + "6-circle-fill": "\f7b4", + "6-circle": "\f7b5", + "6-square-fill": "\f7b6", + "6-square": "\f7b7", + "7-circle-fill": "\f7ba", + "7-circle": "\f7bb", + "7-square-fill": "\f7bc", + "7-square": "\f7bd", + "8-circle-fill": "\f7c0", + "8-circle": "\f7c1", + "8-square-fill": "\f7c2", + "8-square": "\f7c3", + "9-circle-fill": "\f7c6", + "9-circle": "\f7c7", + "9-square-fill": "\f7c8", + "9-square": "\f7c9", + "airplane-engines-fill": "\f7ca", + "airplane-engines": "\f7cb", + "airplane-fill": "\f7cc", + "airplane": "\f7cd", + "alexa": "\f7ce", + "alipay": "\f7cf", + "android": "\f7d0", + "android2": "\f7d1", + "box-fill": "\f7d2", + "box-seam-fill": "\f7d3", + "browser-chrome": "\f7d4", + "browser-edge": "\f7d5", + "browser-firefox": "\f7d6", + "browser-safari": "\f7d7", + "c-circle-fill": "\f7da", + "c-circle": "\f7db", + "c-square-fill": "\f7dc", + "c-square": "\f7dd", + "capsule-pill": "\f7de", + "capsule": "\f7df", + "car-front-fill": "\f7e0", + "car-front": "\f7e1", + "cassette-fill": "\f7e2", + "cassette": "\f7e3", + "cc-circle-fill": "\f7e6", + "cc-circle": "\f7e7", + "cc-square-fill": "\f7e8", + "cc-square": "\f7e9", + "cup-hot-fill": "\f7ea", + "cup-hot": "\f7eb", + "currency-rupee": "\f7ec", + "dropbox": "\f7ed", + "escape": "\f7ee", + "fast-forward-btn-fill": "\f7ef", + "fast-forward-btn": "\f7f0", + "fast-forward-circle-fill": "\f7f1", + "fast-forward-circle": "\f7f2", + "fast-forward-fill": "\f7f3", + "fast-forward": "\f7f4", + "filetype-sql": "\f7f5", + "fire": "\f7f6", + "google-play": "\f7f7", + "h-circle-fill": "\f7fa", + "h-circle": "\f7fb", + "h-square-fill": "\f7fc", + "h-square": "\f7fd", + "indent": "\f7fe", + "lungs-fill": "\f7ff", + "lungs": "\f800", + "microsoft-teams": "\f801", + "p-circle-fill": "\f804", + "p-circle": "\f805", + "p-square-fill": "\f806", + "p-square": "\f807", + "pass-fill": "\f808", + "pass": "\f809", + "prescription": "\f80a", + "prescription2": "\f80b", + "r-circle-fill": "\f80e", + "r-circle": "\f80f", + "r-square-fill": "\f810", + "r-square": "\f811", + "repeat-1": "\f812", + "repeat": "\f813", + "rewind-btn-fill": "\f814", + "rewind-btn": "\f815", + "rewind-circle-fill": "\f816", + "rewind-circle": "\f817", + "rewind-fill": "\f818", + "rewind": "\f819", + "train-freight-front-fill": "\f81a", + "train-freight-front": "\f81b", + "train-front-fill": "\f81c", + "train-front": "\f81d", + "train-lightrail-front-fill": "\f81e", + "train-lightrail-front": "\f81f", + "truck-front-fill": "\f820", + "truck-front": "\f821", + "ubuntu": "\f822", + "unindent": "\f823", + "unity": "\f824", + "universal-access-circle": "\f825", + "universal-access": "\f826", + "virus": "\f827", + "virus2": "\f828", + "wechat": "\f829", + "yelp": "\f82a", + "sign-stop-fill": "\f82b", + "sign-stop-lights-fill": "\f82c", + "sign-stop-lights": "\f82d", + "sign-stop": "\f82e", + "sign-turn-left-fill": "\f82f", + "sign-turn-left": "\f830", + "sign-turn-right-fill": "\f831", + "sign-turn-right": "\f832", + "sign-turn-slight-left-fill": "\f833", + "sign-turn-slight-left": "\f834", + "sign-turn-slight-right-fill": "\f835", + "sign-turn-slight-right": "\f836", + "sign-yield-fill": "\f837", + "sign-yield": "\f838", + "ev-station-fill": "\f839", + "ev-station": "\f83a", + "fuel-pump-diesel-fill": "\f83b", + "fuel-pump-diesel": "\f83c", + "fuel-pump-fill": "\f83d", + "fuel-pump": "\f83e", + "0-circle-fill": "\f83f", + "0-circle": "\f840", + "0-square-fill": "\f841", + "0-square": "\f842", + "rocket-fill": "\f843", + "rocket-takeoff-fill": "\f844", + "rocket-takeoff": "\f845", + "rocket": "\f846", + "stripe": "\f847", + "subscript": "\f848", + "superscript": "\f849", + "trello": "\f84a", + "envelope-at-fill": "\f84b", + "envelope-at": "\f84c", + "regex": "\f84d", + "text-wrap": "\f84e", + "sign-dead-end-fill": "\f84f", + "sign-dead-end": "\f850", + "sign-do-not-enter-fill": "\f851", + "sign-do-not-enter": "\f852", + "sign-intersection-fill": "\f853", + "sign-intersection-side-fill": "\f854", + "sign-intersection-side": "\f855", + "sign-intersection-t-fill": "\f856", + "sign-intersection-t": "\f857", + "sign-intersection-y-fill": "\f858", + "sign-intersection-y": "\f859", + "sign-intersection": "\f85a", + "sign-merge-left-fill": "\f85b", + "sign-merge-left": "\f85c", + "sign-merge-right-fill": "\f85d", + "sign-merge-right": "\f85e", + "sign-no-left-turn-fill": "\f85f", + "sign-no-left-turn": "\f860", + "sign-no-parking-fill": "\f861", + "sign-no-parking": "\f862", + "sign-no-right-turn-fill": "\f863", + "sign-no-right-turn": "\f864", + "sign-railroad-fill": "\f865", + "sign-railroad": "\f866", + "building-add": "\f867", + "building-check": "\f868", + "building-dash": "\f869", + "building-down": "\f86a", + "building-exclamation": "\f86b", + "building-fill-add": "\f86c", + "building-fill-check": "\f86d", + "building-fill-dash": "\f86e", + "building-fill-down": "\f86f", + "building-fill-exclamation": "\f870", + "building-fill-gear": "\f871", + "building-fill-lock": "\f872", + "building-fill-slash": "\f873", + "building-fill-up": "\f874", + "building-fill-x": "\f875", + "building-fill": "\f876", + "building-gear": "\f877", + "building-lock": "\f878", + "building-slash": "\f879", + "building-up": "\f87a", + "building-x": "\f87b", + "buildings-fill": "\f87c", + "buildings": "\f87d", + "bus-front-fill": "\f87e", + "bus-front": "\f87f", + "ev-front-fill": "\f880", + "ev-front": "\f881", + "globe-americas": "\f882", + "globe-asia-australia": "\f883", + "globe-central-south-asia": "\f884", + "globe-europe-africa": "\f885", + "house-add-fill": "\f886", + "house-add": "\f887", + "house-check-fill": "\f888", + "house-check": "\f889", + "house-dash-fill": "\f88a", + "house-dash": "\f88b", + "house-down-fill": "\f88c", + "house-down": "\f88d", + "house-exclamation-fill": "\f88e", + "house-exclamation": "\f88f", + "house-gear-fill": "\f890", + "house-gear": "\f891", + "house-lock-fill": "\f892", + "house-lock": "\f893", + "house-slash-fill": "\f894", + "house-slash": "\f895", + "house-up-fill": "\f896", + "house-up": "\f897", + "house-x-fill": "\f898", + "house-x": "\f899", + "person-add": "\f89a", + "person-down": "\f89b", + "person-exclamation": "\f89c", + "person-fill-add": "\f89d", + "person-fill-check": "\f89e", + "person-fill-dash": "\f89f", + "person-fill-down": "\f8a0", + "person-fill-exclamation": "\f8a1", + "person-fill-gear": "\f8a2", + "person-fill-lock": "\f8a3", + "person-fill-slash": "\f8a4", + "person-fill-up": "\f8a5", + "person-fill-x": "\f8a6", + "person-gear": "\f8a7", + "person-lock": "\f8a8", + "person-slash": "\f8a9", + "person-up": "\f8aa", + "scooter": "\f8ab", + "taxi-front-fill": "\f8ac", + "taxi-front": "\f8ad", + "amd": "\f8ae", + "database-add": "\f8af", + "database-check": "\f8b0", + "database-dash": "\f8b1", + "database-down": "\f8b2", + "database-exclamation": "\f8b3", + "database-fill-add": "\f8b4", + "database-fill-check": "\f8b5", + "database-fill-dash": "\f8b6", + "database-fill-down": "\f8b7", + "database-fill-exclamation": "\f8b8", + "database-fill-gear": "\f8b9", + "database-fill-lock": "\f8ba", + "database-fill-slash": "\f8bb", + "database-fill-up": "\f8bc", + "database-fill-x": "\f8bd", + "database-fill": "\f8be", + "database-gear": "\f8bf", + "database-lock": "\f8c0", + "database-slash": "\f8c1", + "database-up": "\f8c2", + "database-x": "\f8c3", + "database": "\f8c4", + "houses-fill": "\f8c5", + "houses": "\f8c6", + "nvidia": "\f8c7", + "person-vcard-fill": "\f8c8", + "person-vcard": "\f8c9", + "sina-weibo": "\f8ca", + "tencent-qq": "\f8cb", + "wikipedia": "\f8cc", + "alphabet-uppercase": "\f2a5", + "alphabet": "\f68a", + "amazon": "\f68d", + "arrows-collapse-vertical": "\f690", + "arrows-expand-vertical": "\f695", + "arrows-vertical": "\f698", + "arrows": "\f6a2", + "ban-fill": "\f6a3", + "ban": "\f6b6", + "bing": "\f6c2", + "cake": "\f6e0", + "cake2": "\f6ed", + "cookie": "\f6ee", + "copy": "\f759", + "crosshair": "\f769", + "crosshair2": "\f794", + "emoji-astonished-fill": "\f795", + "emoji-astonished": "\f79a", + "emoji-grimace-fill": "\f79b", + "emoji-grimace": "\f7a0", + "emoji-grin-fill": "\f7a1", + "emoji-grin": "\f7a6", + "emoji-surprise-fill": "\f7a7", + "emoji-surprise": "\f7ac", + "emoji-tear-fill": "\f7ad", + "emoji-tear": "\f7b2", + "envelope-arrow-down-fill": "\f7b3", + "envelope-arrow-down": "\f7b8", + "envelope-arrow-up-fill": "\f7b9", + "envelope-arrow-up": "\f7be", + "feather": "\f7bf", + "feather2": "\f7c4", + "floppy-fill": "\f7c5", + "floppy": "\f7d8", + "floppy2-fill": "\f7d9", + "floppy2": "\f7e4", + "gitlab": "\f7e5", + "highlighter": "\f7f8", + "marker-tip": "\f802", + "nvme-fill": "\f803", + "nvme": "\f80c", + "opencollective": "\f80d", + "pci-card-network": "\f8cd", + "pci-card-sound": "\f8ce", + "radar": "\f8cf", + "send-arrow-down-fill": "\f8d0", + "send-arrow-down": "\f8d1", + "send-arrow-up-fill": "\f8d2", + "send-arrow-up": "\f8d3", + "sim-slash-fill": "\f8d4", + "sim-slash": "\f8d5", + "sourceforge": "\f8d6", + "substack": "\f8d7", + "threads-fill": "\f8d8", + "threads": "\f8d9", + "transparency": "\f8da", + "twitter-x": "\f8db", + "type-h4": "\f8dc", + "type-h5": "\f8dd", + "type-h6": "\f8de", + "backpack-fill": "\f8df", + "backpack": "\f8e0", + "backpack2-fill": "\f8e1", + "backpack2": "\f8e2", + "backpack3-fill": "\f8e3", + "backpack3": "\f8e4", + "backpack4-fill": "\f8e5", + "backpack4": "\f8e6", + "brilliance": "\f8e7", + "cake-fill": "\f8e8", + "cake2-fill": "\f8e9", + "duffle-fill": "\f8ea", + "duffle": "\f8eb", + "exposure": "\f8ec", + "gender-neuter": "\f8ed", + "highlights": "\f8ee", + "luggage-fill": "\f8ef", + "luggage": "\f8f0", + "mailbox-flag": "\f8f1", + "mailbox2-flag": "\f8f2", + "noise-reduction": "\f8f3", + "passport-fill": "\f8f4", + "passport": "\f8f5", + "person-arms-up": "\f8f6", + "person-raised-hand": "\f8f7", + "person-standing-dress": "\f8f8", + "person-standing": "\f8f9", + "person-walking": "\f8fa", + "person-wheelchair": "\f8fb", + "shadows": "\f8fc", + "suitcase-fill": "\f8fd", + "suitcase-lg-fill": "\f8fe", + "suitcase-lg": "\f8ff", + "suitcase": "\f900", + "suitcase2-fill": "\f901", + "suitcase2": "\f902", + "vignette": "\f903", + "bluesky": "\f7f9", + "tux": "\f904", + "beaker-fill": "\f905", + "beaker": "\f906", + "flask-fill": "\f907", + "flask-florence-fill": "\f908", + "flask-florence": "\f909", + "flask": "\f90a", + "leaf-fill": "\f90b", + "leaf": "\f90c", + "measuring-cup-fill": "\f90d", + "measuring-cup": "\f90e", + "unlock2-fill": "\f90f", + "unlock2": "\f910", + "battery-low": "\f911", + "anthropic": "\f912", + "apple-music": "\f913", + "claude": "\f914", + "openai": "\f915", + "perplexity": "\f916", + "css": "\f917", + "javascript": "\f918", + "typescript": "\f919", + "fork-knife": "\f91a", + "globe-americas-fill": "\f91b", + "globe-asia-australia-fill": "\f91c", + "globe-central-south-asia-fill": "\f91d", + "globe-europe-africa-fill": "\f91e", +); + +@each $icon, $codepoint in $bootstrap-icons-map { + .bi-#{$icon}::before { content: $codepoint; } +} diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff new file mode 100644 index 00000000..a4fa4f02 Binary files /dev/null and b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff differ diff --git a/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff2 b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff2 new file mode 100644 index 00000000..4d8c490e Binary files /dev/null and b/extensions/pagetop-bootsier/assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff2 differ diff --git a/extensions/pagetop-bootsier/build.rs b/extensions/pagetop-bootsier/build.rs index df7a2750..8de7c387 100644 --- a/extensions/pagetop-bootsier/build.rs +++ b/extensions/pagetop-bootsier/build.rs @@ -1,20 +1,85 @@ -use pagetop_build::StaticFilesBundle; +//! Script de compilacion de activos estaticos. +//! +//! Genera el directorio `static/` a partir de `assets/` y embebe su contenido en el binario: +//! +//! - `static/css/` - CSS compilado a partir de los archivos SCSS de `assets/`. +//! - `static/js/` - JS copiado desde `assets/`, renombrando AdminLTE a `bootsier.min.js`. +//! - `static/fonts/` - Fuentes copiadas desde `assets/`. +//! +//! Los archivos `.map` se copian a `static/js/` para uso en desarrollo pero no se incluyen en el +//! binario embebido. + +use pagetop_build::{StaticFilesBundle, compile_scss, copy_file, copy_file_replacing, minify_js}; -use std::env; use std::path::Path; fn main() -> std::io::Result<()> { - StaticFilesBundle::from_scss("./static/scss/bootsier.scss", "bootstrap.min.css") - .with_name("bootsier_bs") + // Regenera `static/` desde cero sólo si hay cambios en `assets/`. + println!("cargo:rerun-if-changed=assets"); + let _ = std::fs::remove_dir_all("static"); + + // CSS: Bootstrap 5.3.8 + AdminLTE 4.0.0 + Bootstrap Icons 1.13.1. + compile_scss("assets/bootsier.scss", "static/css/bootsier.min.css")?; + + // JS: Bootstrap bundle. + copy_file( + "assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js", + "static/js/bootsier.bundle.min.js", + )?; + copy_file( + "assets/bootstrap-5.3.8/js/bootstrap.bundle.min.js.map", + "static/js/bootsier.bundle.min.js.map", + )?; + // JS: AdminLTE renombrado a bootsier.extended.min.js. + copy_file_replacing( + "assets/adminlte-4.0.0/js/adminlte.min.js", + "static/js/bootsier.extended.min.js", + &[("adminlte.min.js.map", "bootsier.extended.min.js.map")], + )?; + copy_file( + "assets/adminlte-4.0.0/js/adminlte.min.js.map", + "static/js/bootsier.extended.min.js.map", + )?; + // JS: shell de Bootsier. + minify_js( + "assets/bootsier.shell.js", + "static/js/bootsier.shell.min.js", + )?; + + // Fuentes: Bootstrap Icons. + copy_file( + "assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff2", + "static/fonts/bootsier.icons.woff2", + )?; + copy_file( + "assets/bootstrap-icons-1.13.1/fonts/bootstrap-icons.woff", + "static/fonts/bootsier.icons.woff", + )?; + // Fuentes: Source Sans 3 (SIL OFL 1.1). + copy_file( + "assets/adminlte-4.0.0/fonts/SourceSans3VF-Upright.otf.woff2", + "static/fonts/bootsier.font.woff2", + )?; + copy_file( + "assets/adminlte-4.0.0/fonts/SourceSans3VF-Italic.otf.woff2", + "static/fonts/bootsier.font.italic.woff2", + )?; + + // Preparación de los paquetes para embeber en el binario. + StaticFilesBundle::from_dir("./static/css", None) + .with_name("bootsier_css") .build()?; - StaticFilesBundle::from_dir("./static/js", Some(bootstrap_js_files)) + + StaticFilesBundle::from_dir("./static/js", Some(only_js_files)) .with_name("bootsier_js") + .build()?; + + StaticFilesBundle::from_dir("./static/fonts", None) + .with_name("bootsier_fonts") .build() } -fn bootstrap_js_files(path: &Path) -> bool { - let bootstrap_js = "bootstrap.bundle.min.js"; - // No filtra durante el desarrollo, solo en la compilación "release". - env::var("PROFILE").unwrap_or_else(|_| "release".to_string()) != "release" - || path.file_name().is_some_and(|f| f == bootstrap_js) +// Los archivos .map no se embeben en el binario; solo se sirven desde disco en desarrollo. +fn only_js_files(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "js") } diff --git a/extensions/pagetop-bootsier/src/config.rs b/extensions/pagetop-bootsier/src/config.rs index 14ee27e1..dede3c4c 100644 --- a/extensions/pagetop-bootsier/src/config.rs +++ b/extensions/pagetop-bootsier/src/config.rs @@ -9,7 +9,7 @@ //! //! Uso: //! -//! ```rust +//! ```rust,no_run //! # use pagetop::prelude::*; //! use pagetop_bootsier::config; //! @@ -26,12 +26,15 @@ use serde::Deserialize; include_config!(SETTINGS: Settings => [ // [bootsier] "bootsier.max_width" => "1440px", + // [dev] + "dev.bootsier_static_dir" => "", ]); /// Ajustes para la sección [`Bootsier`] de [`SETTINGS`]. #[derive(Debug, Deserialize)] pub struct Settings { pub bootsier: Bootsier, + pub dev: Dev, } /// Sección **`[bootsier]`** de la configuración. Forma parte de [`Settings`]. @@ -40,3 +43,16 @@ pub struct Bootsier { /// Ancho máximo predeterminado para la página, por ejemplo "100%" o "90rem". pub max_width: UnitValue, } + +/// Sección **`[dev]`** de la configuración. Forma parte de [`Settings`]. +#[derive(Debug, Deserialize)] +pub struct Dev { + /// Directorio raíz de `static/` para servir los archivos estáticos propios de Bootsier. + /// + /// Si se indica una ruta válida, absoluta o relativa al directorio del proyecto o del binario + /// en ejecución, los archivos estáticos se servirán desde disco. Útil para poder modificar los + /// archivos estáticos mientras la aplicación está en ejecución, sin necesidad de recompilar. + /// + /// Si la cadena está vacía, se ignora este ajuste. + pub bootsier_static_dir: String, +} diff --git a/extensions/pagetop-bootsier/src/handlers/button.rs b/extensions/pagetop-bootsier/src/handlers/button.rs new file mode 100644 index 00000000..5846283d --- /dev/null +++ b/extensions/pagetop-bootsier/src/handlers/button.rs @@ -0,0 +1,7 @@ +use pagetop::prelude::*; + +pub fn setup(button: &mut Button) { + button + .alter_prop(PropsOp::remove_classes("button")) + .alter_prop(PropsOp::prepend_classes("btn")); +} diff --git a/extensions/pagetop-bootsier/src/handlers/input.rs b/extensions/pagetop-bootsier/src/handlers/input.rs new file mode 100644 index 00000000..94f906c4 --- /dev/null +++ b/extensions/pagetop-bootsier/src/handlers/input.rs @@ -0,0 +1,59 @@ +use pagetop::prelude::*; + +pub fn render(c: &form::input::Field, cx: &mut Context) -> Result { + let container_id = c.id(); + let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); + let floating = c.props().has_class("form-floating"); + let input_class = if *c.plaintext() { + "form-control-plaintext" + } else { + "form-control" + }; + // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido, se + // fuerza `placeholder=""`. + let placeholder = if floating { + Some(c.placeholder().lookup(cx).unwrap_or_default()) + } else { + c.placeholder().lookup(cx) + }; + let label = match c.label().lookup(cx) { + Some(text) => html! { + label for=[input_id.as_deref()] class="form-label" { + (text) + @if *c.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (c.props()) { + @if !floating { (label) } + input + type=(c.kind()) + id=[input_id.as_deref()] + class=(input_class) + name=[c.name().get()] + value=[c.value().get()] + minlength=[c.minlength().get()] + maxlength=[c.maxlength().get()] + placeholder=[placeholder] + inputmode=[c.inputmode().get()] + autocomplete=[c.autocomplete().get()] + autofocus[*c.autofocus()] + readonly[*c.readonly() || *c.plaintext()] + required[*c.required()] + disabled[*c.disabled()]; + @if floating { (label) } + @if let Some(description) = c.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +} diff --git a/extensions/pagetop-bootsier/src/handlers/select.rs b/extensions/pagetop-bootsier/src/handlers/select.rs new file mode 100644 index 00000000..03a48735 --- /dev/null +++ b/extensions/pagetop-bootsier/src/handlers/select.rs @@ -0,0 +1,80 @@ +use pagetop::prelude::*; + +pub fn setup(c: &mut form::select::Field) { + if c.props().has_class("form-floating") { + c.alter_multiple(false); + c.alter_rows(None::); + } +} + +pub fn render(c: &form::select::Field, cx: &mut Context) -> Result { + let container_id = c.id(); + let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); + let floating = c.props().has_class("form-floating"); + let label = match c.label().lookup(cx) { + Some(text) => html! { + label for=[select_id.as_deref()] class="form-label" { + (text) + @if *c.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (c.props()) { + @if !floating { (label) } + select + id=[select_id.as_deref()] + class="form-select" + name=[c.name().get()] + multiple[*c.multiple()] + size=[c.rows().get()] + autocomplete=[c.autocomplete().get()] + autofocus[*c.autofocus()] + required[*c.required()] + disabled[*c.disabled()] + { + @for entry in c.entries() { + @match entry { + form::select::Entry::Item(opt) => { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + form::select::Entry::Group(group) => { + optgroup + label=(group.label().using(cx)) + disabled[*group.disabled()] + { + @for opt in group.items() { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + } + } + } + } + } + @if floating { (label) } + @if let Some(description) = c.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +} diff --git a/extensions/pagetop-bootsier/src/handlers/textarea.rs b/extensions/pagetop-bootsier/src/handlers/textarea.rs new file mode 100644 index 00000000..2f41a765 --- /dev/null +++ b/extensions/pagetop-bootsier/src/handlers/textarea.rs @@ -0,0 +1,63 @@ +use pagetop::prelude::*; + +pub fn setup(c: &mut form::Textarea) { + if c.props().has_class("form-floating") { + c.alter_rows(None::); + } +} + +pub fn render(c: &form::Textarea, cx: &mut Context) -> Result { + let container_id = c.id(); + let textarea_id = container_id + .as_deref() + .map(|id| util::join!(id, "-textarea")); + let floating = c.props().has_class("form-floating"); + // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido se + // fuerza `placeholder=""`. + let placeholder = if floating { + Some(c.placeholder().lookup(cx).unwrap_or_default()) + } else { + c.placeholder().lookup(cx) + }; + let label = match c.label().lookup(cx) { + Some(text) => html! { + label for=[textarea_id.as_deref()] class="form-label" { + (text) + @if *c.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (c.props()) { + @if !floating { (label) } + textarea + id=[textarea_id.as_deref()] + class="form-control" + name=[c.name().get()] + rows=[c.rows().get()] + minlength=[c.minlength().get()] + maxlength=[c.maxlength().get()] + placeholder=[placeholder] + autocomplete=[c.autocomplete().get()] + autofocus[*c.autofocus()] + readonly[*c.readonly()] + required[*c.required()] + disabled[*c.disabled()] + { + @if let Some(value) = c.value().get() { (value) } + } + @if floating { (label) } + @if let Some(description) = c.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +} diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 8c0ec847..23e07722 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -10,7 +10,6 @@ [![Descargas](https://img.shields.io/crates/d/pagetop-bootsier.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-bootsier) [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia) -
## Sobre PageTop @@ -19,8 +18,7 @@ clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -# ⚡️ Guía rápida +## Guía rápida Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`: @@ -46,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: @@ -96,32 +89,11 @@ pub mod config; pub mod theme; -/// Plantillas que Bootsier añade. -#[derive(AutoDefault)] -pub enum BootsierTemplate { - /// Plantilla predeterminada de Bootsier. - #[default] - Standard, -} - -impl Template for BootsierTemplate { - fn render(&'static self, cx: &mut Context) -> Markup { - match self { - Self::Standard => theme::Container::new() - .with_classes(ClassesOp::Add, "container-wrapper") - .with_width(theme::container::Width::FluidMax( - config::SETTINGS.bootsier.max_width, - )) - .with_child(Html::with(|cx| { - html! { - (DefaultRegion::Header.render(cx)) - (DefaultRegion::Content.render(cx)) - (DefaultRegion::Footer.render(cx)) - } - })), - } - .render(cx) - } +mod handlers { + pub mod button; + pub mod input; + pub mod select; + pub mod textarea; } /// Implementa el tema. @@ -141,8 +113,17 @@ impl Extension for Bootsier { } fn configure_router(&self, router: Router) -> Router { - serve_static_files!(router, [bootsier_bs] => "/bootsier/bs"); - serve_static_files!(router, [bootsier_js] => "/bootsier/js"); + let base = &config::SETTINGS.dev.bootsier_static_dir; + let subdir = |s: &str| { + if base.is_empty() { + String::new() + } else { + format!("{base}/{s}") + } + }; + serve_static_files!(router, [subdir("css"), bootsier_css] => "/bootsier/css"); + serve_static_files!(router, [subdir("js"), bootsier_js] => "/bootsier/js"); + serve_static_files!(router, [subdir("fonts"), bootsier_fonts] => "/bootsier/fonts"); router } } @@ -153,16 +134,38 @@ impl Theme for Bootsier { &BootsierTemplate::Standard } + fn handle_component( + &self, + component: &mut dyn Component, + cx: &mut Context, + ) -> Option> { + setup_component!(component, { + Button => |c| handlers::button::setup(c), + form::select::Field => |c| handlers::select::setup(c), + form::Textarea => |c| handlers::textarea::setup(c), + }); + render_component!(component, { + form::input::Field => |c| handlers::input::render(c, cx), + form::select::Field => |c| handlers::select::render(c, cx), + form::Textarea => |c| handlers::textarea::render(c, cx), + }) + } + fn before_render_page_body(&self, page: &mut Page) { page.alter_assets(AssetsOp::AddStyleSheet( - StyleSheet::from("/bootsier/bs/bootstrap.min.css") + StyleSheet::from("/bootsier/css/bootsier.min.css") + .with_version(ADMINLTE_VERSION) + .with_weight(-90), + )) + .alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/bootsier/js/bootsier.bundle.min.js") .with_version(BOOTSTRAP_VERSION) .with_weight(-90), )) .alter_assets(AssetsOp::AddJavaScript( - JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js") - .with_version(BOOTSTRAP_VERSION) - .with_weight(-90), + JavaScript::defer("/bootsier/js/bootsier.extended.min.js") + .with_version(ADMINLTE_VERSION) + .with_weight(-89), )) .alter_child_in( &DefaultRegion::Footer, diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index fb7dd0ed..22e55792 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -11,7 +11,7 @@ pub mod classes; // Button. mod button; -pub use button::Button; +pub use button::{Button, ButtonAction}; // Container. pub mod container; @@ -27,6 +27,12 @@ pub use dropdown::Dropdown; pub mod form; #[doc(inline)] pub use form::Form; +#[doc(hidden)] +pub use form::input::InputBootsier; +#[doc(hidden)] +pub use form::select::SelectBootsier; +#[doc(hidden)] +pub use form::textarea::TextareaBootsier; // Image. pub mod image; diff --git a/extensions/pagetop-bootsier/src/theme/attrs.rs b/extensions/pagetop-bootsier/src/theme/attrs.rs index e5f0a82f..3b3be43a 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs.rs @@ -15,6 +15,3 @@ pub use border::BorderColor; mod rounded; pub use rounded::RoundedRadius; - -mod button; -pub use button::{ButtonAction, ButtonColor, ButtonSize}; diff --git a/extensions/pagetop-bootsier/src/theme/attrs/border.rs b/extensions/pagetop-bootsier/src/theme/attrs/border.rs index af66db78..f83bb13a 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/border.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/border.rs @@ -57,7 +57,7 @@ impl BorderColor { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary"); /// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs b/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs index ec2c8568..5ee64ad5 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/breakpoint.rs @@ -69,7 +69,7 @@ impl BreakPoint { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// let bp = BreakPoint::MD; /// assert_eq!(bp.class_with("col", ""), "col-md"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/button.rs b/extensions/pagetop-bootsier/src/theme/attrs/button.rs deleted file mode 100644 index 01e0dd57..00000000 --- a/extensions/pagetop-bootsier/src/theme/attrs/button.rs +++ /dev/null @@ -1,145 +0,0 @@ -use pagetop::prelude::*; - -use crate::theme::attrs::Color; - -// **< ButtonAction >********************************************************************************* - -/// Comportamiento de un [`Button`](crate::theme::Button) al activarse. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ButtonAction { - /// Envía un formulario al servidor. Es el **tipo por defecto**. - #[default] - Submit, - /// Restablece todos los campos de un formulario a sus valores iniciales. - Reset, - /// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse - /// mediante JavaScript. - Plain, -} - -impl std::fmt::Display for ButtonAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ButtonAction::Submit => "submit", - ButtonAction::Reset => "reset", - ButtonAction::Plain => "button", - }) - } -} - -// **< ButtonColor >******************************************************************************** - -/// Esquema de color para [`Button`](crate::theme::Button). -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ButtonColor { - /// No define ninguna clase. - #[default] - Default, - /// Genera la clase `btn-{color}` (botón sólido). - Background(Color), - /// Genera la clase `btn-outline-{color}` (fondo transparente con contorno coloreado). - Outline(Color), - /// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto. - Link, -} - -impl ButtonColor { - const BTN_PREFIX: &str = "btn-"; - const BTN_OUTLINE_PREFIX: &str = "btn-outline-"; - const BTN_LINK: &str = "btn-link"; - - /// Añade la clase `btn-*` a la cadena de clases. - #[inline] - pub(crate) fn push_class(self, classes: &mut String) { - if let Self::Default = self { - return; - } - if !classes.is_empty() { - classes.push(' '); - } - match self { - Self::Background(c) => { - classes.push_str(Self::BTN_PREFIX); - classes.push_str(c.as_str()); - } - Self::Outline(c) => { - classes.push_str(Self::BTN_OUTLINE_PREFIX); - classes.push_str(c.as_str()); - } - Self::Link => classes.push_str(Self::BTN_LINK), - Self::Default => unreachable!(), - } - } - - /// Devuelve la clase `btn-*` correspondiente al color del botón. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// assert_eq!( - /// ButtonColor::Background(Color::Primary).to_class(), - /// "btn-primary" - /// ); - /// assert_eq!( - /// ButtonColor::Outline(Color::Danger).to_class(), - /// "btn-outline-danger" - /// ); - /// assert_eq!(ButtonColor::Link.to_class(), "btn-link"); - /// assert_eq!(ButtonColor::Default.to_class(), ""); - /// ``` - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_class(&mut class); - class - } -} - -// **< ButtonSize >********************************************************************************* - -/// Tamaño visual de un botón. -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum ButtonSize { - /// Tamaño por defecto del tema (no añade clase). - #[default] - Default, - /// Botón compacto. - Small, - /// Botón grande. - Large, -} - -impl ButtonSize { - const BTN_SM: &str = "btn-sm"; - const BTN_LG: &str = "btn-lg"; - - /// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases. - #[inline] - pub(crate) fn push_class(self, classes: &mut String) { - let class = match self { - Self::Default => return, - Self::Small => Self::BTN_SM, - Self::Large => Self::BTN_LG, - }; - if !classes.is_empty() { - classes.push(' '); - } - classes.push_str(class); - } - - /// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón. - /// - /// # Ejemplos - /// - /// ```rust - /// # use pagetop_bootsier::theme::*; - /// assert_eq!(ButtonSize::Small.to_class(), "btn-sm"); - /// assert_eq!(ButtonSize::Large.to_class(), "btn-lg"); - /// assert_eq!(ButtonSize::Default.to_class(), ""); - /// ``` - pub fn to_class(self) -> String { - let mut class = String::new(); - self.push_class(&mut class); - class - } -} diff --git a/extensions/pagetop-bootsier/src/theme/attrs/color.rs b/extensions/pagetop-bootsier/src/theme/attrs/color.rs index eb9f57d4..de254643 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/color.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/color.rs @@ -43,7 +43,7 @@ impl Color { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(Color::Primary.to_class(), "primary"); /// assert_eq!(Color::Danger.to_class(), "danger"); @@ -123,7 +123,7 @@ impl Opacity { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100"); /// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50"); @@ -155,7 +155,7 @@ impl Opacity { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(Opacity::Opaque.to_class(), "opacity-100"); /// assert_eq!(Opacity::Half.to_class(), "opacity-50"); @@ -236,7 +236,7 @@ impl ColorBg { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(ColorBg::Body.to_class(), "bg-body"); /// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary"); @@ -320,7 +320,7 @@ impl ColorText { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(ColorText::Body.to_class(), "text-body"); /// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/layout.rs b/extensions/pagetop-bootsier/src/theme/attrs/layout.rs index 81b07834..54e3f516 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/layout.rs @@ -60,7 +60,7 @@ impl ScaleSize { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(ScaleSize::Auto.class_with("border"), "border"); /// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0"); diff --git a/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs b/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs index 2a959767..0f9536a8 100644 --- a/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/attrs/rounded.rs @@ -70,7 +70,7 @@ impl RoundedRadius { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2"); /// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0"); @@ -102,7 +102,7 @@ impl RoundedRadius { /// /// # Ejemplos /// - /// ```rust + /// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// assert_eq!(RoundedRadius::Default.to_class(), "rounded"); /// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0"); diff --git a/extensions/pagetop-bootsier/src/theme/button.rs b/extensions/pagetop-bootsier/src/theme/button.rs index e494c1df..eb705ce3 100644 --- a/extensions/pagetop-bootsier/src/theme/button.rs +++ b/extensions/pagetop-bootsier/src/theme/button.rs @@ -1,214 +1 @@ -use pagetop::prelude::*; - -use crate::theme::{ButtonAction, ButtonColor, ButtonSize}; - -/// Componente para crear un **botón**. -/// -/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`, -/// `reset` y botón genérico) y con la variedad de estilos del tema a través de [`ButtonColor`] y -/// [`ButtonSize`]. -/// -/// El comportamiento del botón se establece al crearlo: -/// -/// - [`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. -/// -/// El botón puede usarse dentro o fuera de un formulario. -/// -/// # Ejemplo -/// -/// ```rust -/// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; -/// -/// let save = Button::submit(L10n::n("Save")) -/// .with_color(ButtonColor::Background(Color::Primary)); -/// -/// let cancel = Button::plain(L10n::n("Cancel")) -/// .with_color(ButtonColor::Outline(Color::Secondary)); -/// -/// let clear = Button::reset(L10n::n("Clear")) -/// .with_size(ButtonSize::Small); -/// ``` -/// -/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del -/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar -/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// #[serde(default)] -/// action: Option, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`. -/// } -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Button { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del botón. - classes: Classes, - /// Devuelve el comportamiento del botón al activarse. - kind: ButtonAction, - /// Devuelve el esquema de color del botón. - color: ButtonColor, - /// Devuelve el tamaño visual del botón. - size: ButtonSize, - /// Devuelve el nombre del botón. - name: AttrName, - /// Devuelve el valor del botón. - value: AttrValue, - /// Devuelve la etiqueta del botón. - label: Attr, - /// Devuelve si el botón recibe el foco automáticamente al cargar la página. - autofocus: bool, - /// Devuelve si el botón está deshabilitado. - disabled: bool, -} - -impl Component for Button { - fn new() -> Self { - Self::default() - } - - fn id(&self) -> Option { - self.id.get() - } - - fn setup(&mut self, _cx: &Context) { - let mut classes = "btn".to_string(); - (*self.color()).push_class(&mut classes); - (*self.size()).push_class(&mut classes); - self.alter_classes(ClassesOp::Prepend, classes); - } - - fn prepare(&self, cx: &mut Context) -> Result { - Ok(html! { - button - id=[self.id()] - type=(self.kind()) - class=[self.classes().get()] - name=[self.name().get()] - value=[self.value().get()] - autofocus[*self.autofocus()] - disabled[*self.disabled()] - { - @if let Some(label) = self.label().lookup(cx) { - (label) - } - } - }) - } -} - -impl Button { - /// Crea un botón de **envío** (`type="submit"`). - /// - /// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los - /// datos al servidor. - pub fn submit(label: L10n) -> Self { - Self { - kind: ButtonAction::Submit, - label: Attr::some(label), - ..Default::default() - } - } - - /// Crea un botón de **restablecimiento** (`type="reset"`). - /// - /// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales. - pub fn reset(label: L10n) -> Self { - Self { - kind: ButtonAction::Reset, - label: Attr::some(label), - ..Default::default() - } - } - - /// Crea un **botón genérico** (`type="button"`). - /// - /// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede - /// definirse mediante JavaScript. - pub fn plain(label: L10n) -> Self { - Self { - kind: ButtonAction::Plain, - label: Attr::some(label), - ..Default::default() - } - } - - // **< Button BUILDER >************************************************************************* - - /// Establece el identificador único (`id`) del botón. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - - /// Modifica la lista de clases CSS aplicadas al botón. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); - self - } - - /// Establece el esquema de color del botón. - /// - /// Usa [`ButtonColor::Background`] para botones sólidos o [`ButtonColor::Outline`] para - /// variantes con contorno. - #[builder_fn] - pub fn with_color(mut self, color: ButtonColor) -> Self { - self.color = color; - self - } - - /// Establece el tamaño visual del botón. - #[builder_fn] - pub fn with_size(mut self, size: ButtonSize) -> Self { - self.size = size; - self - } - - /// Establece el nombre del botón (atributo `name`). - /// - /// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par - /// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado. - #[builder_fn] - pub fn with_name(mut self, name: impl AsRef) -> Self { - self.name.alter_name(name); - self - } - - /// Establece el valor del botón (atributo `value`). - /// - /// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón - /// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario. - #[builder_fn] - pub fn with_value(mut self, value: impl AsRef) -> Self { - self.value.alter_str(value); - self - } - - /// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla). - #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); - self - } - - /// Establece si el botón recibe el foco automáticamente al cargar la página. - #[builder_fn] - pub fn with_autofocus(mut self, autofocus: bool) -> Self { - self.autofocus = autofocus; - self - } - - /// Establece si el botón está deshabilitado. - #[builder_fn] - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} +pub use pagetop::base::component::{Button, ButtonAction}; diff --git a/extensions/pagetop-bootsier/src/theme/classes.rs b/extensions/pagetop-bootsier/src/theme/classes.rs index 9e6c234d..2009922a 100644 --- a/extensions/pagetop-bootsier/src/theme/classes.rs +++ b/extensions/pagetop-bootsier/src/theme/classes.rs @@ -3,6 +3,9 @@ mod color; pub use color::{Background, Text}; +mod button; +pub use button::{ButtonColor, ButtonSize}; + mod border; pub use border::Border; diff --git a/extensions/pagetop-bootsier/src/theme/classes/border.rs b/extensions/pagetop-bootsier/src/theme/classes/border.rs index bfa65522..7085b2e6 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/border.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/border.rs @@ -26,7 +26,7 @@ use crate::theme::attrs::{BorderColor, Opacity, ScaleSize, Side}; /// /// # Ejemplos /// -/// ```rust +/// ```rust,no_run /// use pagetop_bootsier::theme::*; /// /// // Borde global. @@ -145,7 +145,7 @@ impl Border { /// /// # Ejemplos /// -/// ```rust +/// ```rust,no_run /// # use pagetop_bootsier::theme::*; /// // Convertir explícitamente con `From::from`: /// let b = classes::Border::from(ScaleSize::Two); diff --git a/extensions/pagetop-bootsier/src/theme/classes/button.rs b/extensions/pagetop-bootsier/src/theme/classes/button.rs new file mode 100644 index 00000000..93a8712e --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/classes/button.rs @@ -0,0 +1,170 @@ +use pagetop::prelude::*; + +use crate::theme::attrs::Color; + +// **< ButtonColor >******************************************************************************** + +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +enum ButtonColorStyle { + #[default] + None, + Solid, + Outline, + Link, +} + +/// Clases para establecer el **color y estilo** de los botones. +/// +/// # Ejemplos +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// // Botón sólido. +/// let save = Button::submit(L10n::n("Save")) +/// .with_prop(PropsOp::add_classes(classes::ButtonColor::solid(Color::Primary))); +/// +/// // Botón con contorno. +/// let cancel = Button::plain(L10n::n("Cancel")) +/// .with_prop(PropsOp::add_classes(classes::ButtonColor::outline(Color::Secondary))); +/// +/// // Botón tipo enlace. +/// let back = Button::plain(L10n::n("Back")) +/// .with_prop(PropsOp::add_classes(classes::ButtonColor::link())); +/// ``` +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub struct ButtonColor { + style: ButtonColorStyle, + color: Color, +} + +impl ButtonColor { + /// Sin clase de color (estilo por defecto del tema). + pub fn new() -> Self { + Self::default() + } + + /// Botón sólido: genera la clase `btn-{color}`. + pub fn solid(color: Color) -> Self { + Self { + style: ButtonColorStyle::Solid, + color, + ..Default::default() + } + } + + /// Botón con contorno: genera la clase `btn-outline-{color}`. + pub fn outline(color: Color) -> Self { + Self { + style: ButtonColorStyle::Outline, + color, + ..Default::default() + } + } + + /// Botón tipo enlace: genera la clase `btn-link`. + pub fn link() -> Self { + Self { + style: ButtonColorStyle::Link, + ..Default::default() + } + } + + // **< ButtonColor BUILDER >******************************************************************** + + /// Cambia el color aplicado al botón (`btn-*` o `btn-outline-*`). + pub fn with_color(mut self, color: Color) -> Self { + self.color = color; + self + } + + // **< ButtonColor HELPERS >******************************************************************** + + /// Devuelve la clase `btn-*` correspondiente al color del botón. + /// + /// Si no se ha definido ningún estilo, devuelve `""`. + pub fn to_class(self) -> String { + match self.style { + ButtonColorStyle::None => String::new(), + ButtonColorStyle::Solid => format!("btn-{}", self.color.as_str()), + ButtonColorStyle::Outline => format!("btn-outline-{}", self.color.as_str()), + ButtonColorStyle::Link => "btn-link".to_owned(), + } + } +} + +impl Into for ButtonColor { + fn into(self) -> CowStr { + self.to_class().into() + } +} + +// **< ButtonSize >********************************************************************************* + +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +enum ButtonSizeVariant { + #[default] + None, + Small, + Large, +} + +/// Clases para establecer el **tamaño** de los botones. +/// +/// # Ejemplos +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_bootsier::theme::*; +/// +/// let small = Button::submit(L10n::n("Save")) +/// .with_prop(PropsOp::add_classes(classes::ButtonSize::small())); +/// +/// let large = Button::submit(L10n::n("Save")) +/// .with_prop(PropsOp::add_classes(classes::ButtonSize::large())); +/// ``` +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub struct ButtonSize { + size: ButtonSizeVariant, +} + +impl ButtonSize { + /// Sin clase de tamaño (tamaño por defecto del tema). + pub fn new() -> Self { + Self::default() + } + + /// Botón compacto: genera la clase `btn-sm`. + pub fn small() -> Self { + Self { + size: ButtonSizeVariant::Small, + } + } + + /// Botón grande: genera la clase `btn-lg`. + pub fn large() -> Self { + Self { + size: ButtonSizeVariant::Large, + } + } + + // **< ButtonSize HELPERS >********************************************************************* + + /// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón. + /// + /// Si no se ha definido ningún tamaño, devuelve `""`. + pub fn to_class(self) -> String { + match self.size { + ButtonSizeVariant::None => String::new(), + ButtonSizeVariant::Small => "btn-sm".to_owned(), + ButtonSizeVariant::Large => "btn-lg".to_owned(), + } + } +} + +impl Into for ButtonSize { + fn into(self) -> CowStr { + self.to_class().into() + } +} diff --git a/extensions/pagetop-bootsier/src/theme/classes/layout.rs b/extensions/pagetop-bootsier/src/theme/classes/layout.rs index 1438b210..42612fec 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/layout.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/layout.rs @@ -9,7 +9,7 @@ use crate::theme::attrs::{ScaleSize, Side}; /// /// # Ejemplos /// -/// ```rust +/// ```rust,no_run /// use pagetop_bootsier::theme::*; /// /// let m = classes::Margin::with(Side::Top, ScaleSize::Three); @@ -97,7 +97,7 @@ impl Margin { /// /// # Ejemplos /// -/// ```rust +/// ```rust,no_run /// use pagetop_bootsier::theme::*; /// /// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two); diff --git a/extensions/pagetop-bootsier/src/theme/classes/rounded.rs b/extensions/pagetop-bootsier/src/theme/classes/rounded.rs index 9fcb544e..d4fbdd2f 100644 --- a/extensions/pagetop-bootsier/src/theme/classes/rounded.rs +++ b/extensions/pagetop-bootsier/src/theme/classes/rounded.rs @@ -14,7 +14,7 @@ use crate::theme::attrs::RoundedRadius; /// /// # Ejemplos /// -/// ```rust +/// ```rust,no_run /// use pagetop_bootsier::theme::*; /// /// // Radio global: diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index 9e4e33c0..07a835a7 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -11,7 +11,7 @@ use crate::theme::*; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// use pagetop_bootsier::theme::*; /// /// let main = Container::main() @@ -20,10 +20,8 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Container { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al contenedor. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el tipo semántico del contenedor. container_kind: container::Kind, /// Devuelve el comportamiento para el ancho del contenedor. @@ -38,11 +36,11 @@ impl Component for Container { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, self.container_width().to_class()); + self.alter_prop(PropsOp::prepend_classes(self.container_width().to_class())); } fn prepare(&self, cx: &mut Context) -> Result { @@ -58,32 +56,32 @@ impl Component for Container { }; Ok(match self.container_kind() { container::Kind::Default => html! { - div id=[self.id()] class=[self.classes().get()] style=[style] { + div (self.props()) style=[style] { (output) } }, container::Kind::Main => html! { - main id=[self.id()] class=[self.classes().get()] style=[style] { + main (self.props()) style=[style] { (output) } }, container::Kind::Header => html! { - header id=[self.id()] class=[self.classes().get()] style=[style] { + header (self.props()) style=[style] { (output) } }, container::Kind::Footer => html! { - footer id=[self.id()] class=[self.classes().get()] style=[style] { + footer (self.props()) style=[style] { (output) } }, container::Kind::Section => html! { - section id=[self.id()] class=[self.classes().get()] style=[style] { + section (self.props()) style=[style] { (output) } }, container::Kind::Article => html! { - article id=[self.id()] class=[self.classes().get()] style=[style] { + article (self.props()) style=[style] { (output) } }, @@ -134,14 +132,14 @@ impl Container { // **< Container BUILDER >********************************************************************** - /// Establece el identificador único (`id`) del contenedor. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al contenedor. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// @@ -150,8 +148,8 @@ impl Container { /// - Establecer bordes ([`classes::Border`]). /// - Redondear las esquinas ([`classes::Rounded`]). #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index b70fed65..e0231b4a 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -21,13 +21,13 @@ use crate::theme::*; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// use pagetop_bootsier::theme::*; /// /// let dd = Dropdown::new() /// .with_title(L10n::n("Menu")) -/// .with_button_color(ButtonColor::Background(Color::Secondary)) +/// .with_button_color(classes::ButtonColor::solid(Color::Secondary)) /// .with_auto_close(dropdown::AutoClose::ClickableInside) /// .with_direction(dropdown::Direction::Dropend) /// .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into())) @@ -38,16 +38,14 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al menú desplegable. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el título del menú desplegable. title: L10n, /// Devuelve el tamaño configurado del botón. - button_size: ButtonSize, + button_size: classes::ButtonSize, /// Devuelve el color/estilo configurado del botón. - button_color: ButtonColor, + button_color: classes::ButtonColor, /// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*). button_split: bool, /// Devuelve si el botón del menú está integrado en un grupo de botones. @@ -70,14 +68,13 @@ impl Component for Dropdown { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes( - ClassesOp::Prepend, + self.alter_prop(PropsOp::prepend_classes( self.direction().class_with(*self.button_grouped()), - ); + )); } fn prepare(&self, cx: &mut Context) -> Result { @@ -91,23 +88,25 @@ impl Component for Dropdown { let title = self.title().using(cx); Ok(html! { - div id=[self.id()] class=[self.classes().get()] { + div (self.props()) { @if !title.is_empty() { - @let mut btn_classes = Classes::new({ - let mut classes = "btn".to_string(); - self.button_size().push_class(&mut classes); - self.button_color().push_class(&mut classes); + @let btn_base = { + let size = self.button_size().to_class(); + let color = self.button_color().to_class(); + let mut classes = String::from("btn"); + if !size.is_empty() { classes.push(' '); classes.push_str(&size); } + if !color.is_empty() { classes.push(' '); classes.push_str(&color); } classes - }); + }; @let pos = self.menu_position(); @let offset = pos.data_offset(); @let reference = pos.data_reference(); @let auto_close = self.auto_close.as_str(); - @let menu_classes = Classes::new({ + @let menu_classes = { let mut classes = "dropdown-menu".to_string(); self.menu_align().push_class(&mut classes); classes - }); + }; // Renderizado en modo split (dos botones) o simple (un botón). @if *self.button_split() { @@ -115,18 +114,18 @@ impl Component for Dropdown { @let btn = html! { button type="button" - class=[btn_classes.get()] + class=(&btn_base) { (title) } }; // Botón *toggle* que abre/cierra el menú asociado. + @let btn_toggle_classes = + util::join!(&btn_base, " dropdown-toggle dropdown-toggle-split"); @let btn_toggle = html! { button type="button" - class=[btn_classes.alter_classes( - ClassesOp::Add, "dropdown-toggle dropdown-toggle-split" - ).get()] + class=(&btn_toggle_classes) data-bs-toggle="dropdown" data-bs-offset=[offset] data-bs-reference=[reference] @@ -142,22 +141,21 @@ impl Component for Dropdown { @match self.direction() { dropdown::Direction::Dropstart => { (btn_toggle) - ul class=[menu_classes.get()] { (items) } + ul class=(&menu_classes) { (items) } (btn) } _ => { (btn) (btn_toggle) - ul class=[menu_classes.get()] { (items) } + ul class=(&menu_classes) { (items) } } } } @else { // Botón único con funcionalidad de *toggle*. + @let btn_toggle_classes = util::join!(&btn_base, " dropdown-toggle"); button type="button" - class=[btn_classes.alter_classes( - ClassesOp::Add, "dropdown-toggle" - ).get()] + class=(&btn_toggle_classes) data-bs-toggle="dropdown" data-bs-offset=[offset] data-bs-reference=[reference] @@ -166,7 +164,7 @@ impl Component for Dropdown { { (title) } - ul class=[menu_classes.get()] { (items) } + ul class=(&menu_classes) { (items) } } } @else { // Sin botón: sólo el listado como menú contextual. @@ -180,17 +178,17 @@ impl Component for Dropdown { impl Dropdown { // **< Dropdown BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del menú desplegable. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al menú desplegable. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } @@ -203,14 +201,14 @@ impl Dropdown { /// Ajusta el tamaño del botón. #[builder_fn] - pub fn with_button_size(mut self, size: ButtonSize) -> Self { + pub fn with_button_size(mut self, size: classes::ButtonSize) -> Self { self.button_size = size; self } /// Define el color/estilo del botón. #[builder_fn] - pub fn with_button_color(mut self, color: ButtonColor) -> Self { + pub fn with_button_color(mut self, color: classes::ButtonColor) -> Self { self.button_color = color; self } diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs index 379a16c9..5eaecc00 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs @@ -45,10 +45,8 @@ pub enum ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al elemento. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, } @@ -59,7 +57,7 @@ impl Component for Item { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, cx: &mut Context) -> Result { @@ -67,7 +65,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { span class="dropdown-item-text" { (label.using(cx)) } @@ -101,7 +99,7 @@ impl Component for Item { let tabindex = disabled.then_some("-1"); html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { a class=(classes) href=[href] @@ -127,7 +125,7 @@ impl Component for Item { let disabled_attr = disabled.then_some("disabled"); html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { button class=(classes) type="button" @@ -141,7 +139,7 @@ impl Component for Item { } ItemKind::Header(label) => html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { h6 class="dropdown-header" { (label.using(cx)) } @@ -149,7 +147,7 @@ impl Component for Item { }, ItemKind::Divider => html! { - li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} } + li (self.props()) { hr class="dropdown-divider" {} } }, }) } @@ -260,17 +258,17 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del elemento. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al elemento. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } } diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs index 82c603ef..c088a86c 100644 --- a/extensions/pagetop-bootsier/src/theme/form.rs +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -1,30 +1,24 @@ //! Definiciones para crear formularios ([`Form`]). -mod props; -pub use props::{Autocomplete, AutofillField, CheckboxKind, Method}; +pub use pagetop::base::component::form::{Autocomplete, AutofillField, CheckboxKind, Method}; -mod component; -pub use component::Form; +pub use pagetop::base::component::form::Form; -mod fieldset; -pub use fieldset::Fieldset; +pub use pagetop::base::component::form::Fieldset; -mod checkbox; -pub use checkbox::Checkbox; +pub use pagetop::base::component::form::Checkbox; -pub mod check; +pub use pagetop::base::component::form::check; -pub mod radio; +pub use pagetop::base::component::form::radio; pub mod select; pub mod input; -mod textarea; +pub mod textarea; pub use textarea::Textarea; -mod range; -pub use range::Range; +pub use pagetop::base::component::form::Range; -mod hidden; -pub use hidden::Hidden; +pub use pagetop::base::component::form::Hidden; diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index 68cce931..c0d35dd9 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -2,452 +2,39 @@ use pagetop::prelude::*; -use crate::LOCALES_BOOTSIER; -use crate::theme::form; +pub use pagetop::base::component::form::input::{Field, Kind, Mode}; -use std::fmt; - -// **< Kind >*************************************************************************************** - -/// Tipo de campo para un [`form::input::Field`]. +/// Extensión de Bootsier para [`form::input::Field`]. /// -/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar -/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text), -/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email), -/// [`telephone()`](Field::telephone) o [`url()`](Field::url). -#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] -pub enum Kind { - /// Entrada de texto genérico (`type="text"`). Es el tipo por defecto. - #[default] - Text, - /// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado. - Password, - /// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda. - Search, - /// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo. - Email, - /// Entrada de un teléfono (`type="tel"`). Activa el teclado de llamadas en móviles. - Telephone, - /// Entrada de una URL (`type="url"`). Comprueba que la entrada sea una URL bien formada. - Url, -} - -impl fmt::Display for Kind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Kind::Text => "text", - Kind::Password => "password", - Kind::Search => "search", - Kind::Email => "email", - Kind::Telephone => "tel", - Kind::Url => "url", - }) - } -} - -// **< Mode >*************************************************************************************** - -/// Sugerencia para el teclado virtual de un [`form::input::Field`]. +/// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se +/// superpone al control mientras está vacío y permanece flotante cuando tiene contenido o está +/// enfocado. /// -/// Indica al navegador qué tipo de teclado virtual mostrar en dispositivos móviles o táctiles al -/// editar el campo. A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los -/// valores aceptados ni activa la validación del navegador; es sólo una sugerencia de presentación. +/// ```rust,no_run +/// # use pagetop::locale::L10n; +/// use pagetop_bootsier::theme::*; /// -/// Se establece con [`form::input::Field::with_inputmode()`]. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Mode { - /// Suprime el teclado virtual. Útil en campos con teclado personalizado basado en JavaScript. - None, - /// Teclado de texto genérico. - Text, - /// Teclado decimal, con dígitos y separador decimal. - Decimal, - /// Teclado numérico, con sólo dígitos. - Numeric, - /// Teclado de teléfono, con dígitos y símbolos `+`, `*` y `#`. - Tel, - /// Teclado optimizado para búsquedas (puede incluir tecla de búsqueda). - Search, - /// Teclado optimizado para correo electrónico (incluye `@` y `.`). - Email, - /// Teclado optimizado para URL (incluye `/`, `.` y `.com`). - Url, -} - -impl fmt::Display for Mode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Mode::None => "none", - Mode::Text => "text", - Mode::Decimal => "decimal", - Mode::Numeric => "numeric", - Mode::Tel => "tel", - Mode::Search => "search", - Mode::Email => "email", - Mode::Url => "url", - }) - } -} - -// **< Field >************************************************************************************** - -/// Componente para crear un **campo de texto de una línea**. -/// -/// Renderiza los tipos más habituales en formularios: -/// -/// - [`form::input::Field::text()`]: campo de texto genérico (`type="text"`, por defecto). -/// - [`form::input::Field::password()`]: contraseña (`type="password"`). -/// - [`form::input::Field::search()`]: búsqueda (`type="search"`). -/// - [`form::input::Field::email()`]: correo electrónico (`type="email"`). -/// - [`form::input::Field::telephone()`]: teléfono (`type="tel"`). -/// - [`form::input::Field::url()`]: URL (`type="url"`). -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let email = form::input::Field::email() -/// .with_name("email") -/// .with_label(L10n::n("Email address")) -/// .with_placeholder(L10n::n("user@example.com")) -/// .with_autocomplete(Some(form::Autocomplete::email())) -/// .with_required(true); +/// let nombre = form::input::Field::text() +/// .with_name("name") +/// .with_label(L10n::n("Name")) +/// .with_placeholder(L10n::n("Enter your name")) +/// .with_floating_label(true); /// ``` -/// -/// Al enviar el formulario el navegador transmite `name=valor`. Un campo de texto siempre envía su -/// valor, incluso si está vacío. En el servidor se deserializa como `String`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// email: String, // Siempre presente; cadena vacía si el usuario no escribió nada. -/// } -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del campo. - classes: Classes, - /// Devuelve el tipo de campo. - kind: Kind, - /// Devuelve el nombre del campo. - name: AttrName, - /// Devuelve el valor inicial del campo. - value: AttrValue, - /// Devuelve la etiqueta del campo. - label: Attr, - /// Devuelve si la etiqueta se muestra flotante sobre el campo. - floating_label: bool, - /// Devuelve el texto de ayuda del campo. - help_text: Attr, - /// Devuelve la longitud mínima permitida en caracteres. - minlength: Attr, - /// Devuelve la longitud máxima permitida en caracteres. - maxlength: Attr, - /// Devuelve el texto indicativo del campo. - placeholder: Attr, - /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, - /// Devuelve si el campo recibe el foco automáticamente al cargar la página. - autofocus: bool, - /// Devuelve si el campo es de sólo lectura. - readonly: bool, - /// Devuelve si el campo es obligatorio. - required: bool, - /// Devuelve si el campo está deshabilitado. - disabled: bool, - /// Devuelve si el campo se muestra como texto plano sin bordes ni fondo. - plaintext: bool, - /// Devuelve la sugerencia de teclado virtual para el campo. - inputmode: Attr, -} - -impl Component for Field { - fn new() -> Self { - Self::default() - } - - fn id(&self) -> Option { - self.id.get() - } - - fn setup(&mut self, _cx: &Context) { - if *self.floating_label() { - self.alter_classes(ClassesOp::Prepend, "form-floating"); - } - self.alter_classes( - ClassesOp::Prepend, - util::join!("form-field form-field-", self.kind().to_string()), - ); - } - - fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); - let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); - let input_class = if *self.plaintext() { - "form-control-plaintext" - } else { - "form-control" - }; - // La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo - // está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`. - let placeholder = if *self.floating_label() { - Some(self.placeholder().lookup(cx).unwrap_or_default()) - } else { - self.placeholder().lookup(cx) - }; - let label = match self.label().lookup(cx) { - Some(text) => html! { - label for=[input_id.as_deref()] class="form-label" { - (text) - @if *self.required() { - span - class="form-required" - title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div id=[container_id.as_deref()] class=[self.classes().get()] { - @if !*self.floating_label() { - (label) - } - input - type=(self.kind()) - id=[input_id.as_deref()] - class=(input_class) - name=[self.name().get()] - value=[self.value().get()] - minlength=[self.minlength().get()] - maxlength=[self.maxlength().get()] - placeholder=[placeholder] - inputmode=[self.inputmode().get()] - autocomplete=[self.autocomplete().get()] - autofocus[*self.autofocus()] - readonly[*self.readonly() || *self.plaintext()] - required[*self.required()] - disabled[*self.disabled()]; - @if *self.floating_label() { - (label) - } - @if let Some(description) = self.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) - } -} - -impl Field { - /// Crea un campo de **texto genérico** (`type="text"`). - /// - /// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada - /// textual sin restricciones de formato específicas. - pub fn text() -> Self { - Self::default() - } - - /// Crea un campo de **contraseña** (`type="password"`). - /// - /// El navegador oculta los caracteres introducidos. Se recomienda usar con - /// [`with_autocomplete()`](Self::with_autocomplete) para permitir autorrellenar con una - /// contraseña guardada o dejar al usuario recibir sugerencias o crear una nueva. - pub fn password() -> Self { - Self { - kind: Kind::Password, - ..Default::default() - } - } - - /// Crea un campo de **búsqueda** (`type="search"`). - /// - /// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos - /// navegadores añaden un botón para borrar el contenido. - pub fn search() -> Self { - Self { - kind: Kind::Search, - ..Default::default() - } - } - - /// Crea un campo de **correo electrónico** (`type="email"`). - /// - /// El navegador valida el formato de la dirección antes de enviar el formulario. En - /// dispositivos móviles muestra un teclado adaptado para introducir direcciones de correo. - pub fn email() -> Self { - Self { - kind: Kind::Email, - ..Default::default() - } - } - - /// Crea un campo de **teléfono** (`type="tel"`). - /// - /// No impone ninguna restricción de formato (los formatos de teléfono varían por país), pero - /// en dispositivos móviles muestra el teclado numérico de llamadas. - pub fn telephone() -> Self { - Self { - kind: Kind::Telephone, - ..Default::default() - } - } - - /// Crea un campo de **URL** (`type="url"`). - /// - /// El navegador valida que el valor sea una URL bien formada antes de enviar el formulario. - pub fn url() -> Self { - Self { - kind: Kind::Url, - ..Default::default() - } - } - - // **< Field BUILDER >************************************************************************** - - /// Establece el identificador único (`id`) del contenedor del campo. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - - /// Modifica la lista de clases CSS aplicadas al contenedor del campo. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); - self - } - - /// Establece el nombre del campo (atributo `name`). - /// - /// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para - /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. - #[builder_fn] - pub fn with_name(mut self, name: impl AsRef) -> Self { - self.name.alter_name(name); - self - } - - /// Establece el valor inicial del campo. - #[builder_fn] - pub fn with_value(mut self, value: impl AsRef) -> Self { - self.value.alter_str(value); - self - } - - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). - #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); - self - } - +pub trait InputBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// /// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene - /// contenido. - #[builder_fn] - pub fn with_floating_label(mut self, floating_label: bool) -> Self { - self.floating_label = floating_label; - self - } + /// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se + /// especifica, se fuerza `placeholder=""` antes del renderizado. + fn with_floating_label(self, floating: bool) -> Self; +} - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). - #[builder_fn] - pub fn with_help_text(mut self, help_text: impl Into>) -> Self { - self.help_text.alter_opt(help_text.into()); - self - } - - /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). - #[builder_fn] - pub fn with_minlength(mut self, minlength: Option) -> Self { - self.minlength.alter_opt(minlength); - self - } - - /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). - #[builder_fn] - pub fn with_maxlength(mut self, maxlength: Option) -> Self { - self.maxlength.alter_opt(maxlength); - self - } - - /// Establece o elimina el texto indicativo del campo (`None` para quitarlo). - /// - /// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir. - /// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo. - #[builder_fn] - pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { - self.placeholder.alter_opt(placeholder.into()); - self - } - - /// Establece la configuración de autocompletado del campo. - /// - /// Usar los métodos de [`form::Autocomplete`] para los valores más habituales (p. ej. - /// [`Autocomplete::email()`](form::Autocomplete::email) o - /// [`Autocomplete::current_password()`](form::Autocomplete::current_password)). - #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); - self - } - - /// Establece si el campo recibe el foco automáticamente al cargar la página. - #[builder_fn] - pub fn with_autofocus(mut self, autofocus: bool) -> Self { - self.autofocus = autofocus; - self - } - - /// Establece si el campo es de sólo lectura. - #[builder_fn] - pub fn with_readonly(mut self, readonly: bool) -> Self { - self.readonly = readonly; - self - } - - /// Establece si el campo es obligatorio. - #[builder_fn] - pub fn with_required(mut self, required: bool) -> Self { - self.required = required; - self - } - - /// Establece si el campo está deshabilitado. - #[builder_fn] - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Establece si el campo se muestra como texto plano (sin bordes ni fondo). - /// - /// Útil para mostrar un valor no editable en pantalla que sí se envía al servidor con el - /// formulario. - #[builder_fn] - pub fn with_plaintext(mut self, plaintext: bool) -> Self { - self.plaintext = plaintext; - self - } - - /// Establece el modo de entrada sugerido para el teclado virtual en dispositivos móviles. - /// - /// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados - /// ni activa la validación del navegador; es sólo una sugerencia de presentación. - #[builder_fn] - pub fn with_inputmode(mut self, inputmode: Option) -> Self { - self.inputmode.alter_opt(inputmode); - self +impl InputBootsier for Field { + fn with_floating_label(self, floating: bool) -> Self { + if floating { + self.with_prop(PropsOp::add_classes("form-floating")) + } else { + self.with_prop(PropsOp::remove_classes("form-floating")) + } } } diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 7d51e9c9..15b4f881 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -2,458 +2,45 @@ use pagetop::prelude::*; -use crate::LOCALES_BOOTSIER; -use crate::theme::form; +pub use pagetop::base::component::form::select::{Entry, Field, Group, Item}; -// **< Item >*************************************************************************************** - -/// Elemento individual de [`form::select::Field`] o de [`form::select::Group`]. +/// Extensión de Bootsier para [`form::select::Field`]. /// -/// Representa un elemento dentro de una lista de selección o de un grupo de elementos de la lista. -/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el -/// usuario. +/// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se +/// superpone al control mientras no hay ninguna opción seleccionada y permanece flotante cuando hay +/// una selección activa. /// -/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Self::with_selected) o -/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Self::with_disabled). +/// ```rust,no_run +/// # use pagetop::locale::L10n; +/// # use pagetop::core::component::Component; +/// use pagetop_bootsier::theme::*; /// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Item { - /// Devuelve el valor enviado al servidor cuando se selecciona el elemento. - value: AttrValue, - /// Devuelve la etiqueta visible del elemento. - label: L10n, - /// Devuelve si el elemento debe aparecer seleccionado por defecto. - selected: bool, - /// Devuelve si el elemento está deshabilitado. - disabled: bool, -} - -impl Item { - /// Crea un nuevo elemento con el valor y la etiqueta indicados. - pub fn new(value: impl AsRef, label: L10n) -> Self { - Self { - value: AttrValue::new(value), - label, - selected: false, - disabled: false, - } - } - - // **< Item BUILDER >*************************************************************************** - - /// Establece si el elemento aparece seleccionado por defecto. - /// - /// En una lista de selección única, el navegador aplica la selección al último elemento marcado - /// si hay más de uno; mientras que en una lista múltiple se respetan todos los elementos - /// marcados. - pub fn with_selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - /// Establece si el elemento está deshabilitado. - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -// **< Group >************************************************************************************** - -/// Grupo de elementos dentro de [`form::select::Field`]. -/// -/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El -/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Self::with_disabled). -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let group = form::select::Group::new(L10n::n("Europe")) -/// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) -/// .with_item(form::select::Item::new("fr", L10n::n("French"))); -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Group { - /// Devuelve la etiqueta visible del grupo de elementos. - label: L10n, - /// Devuelve los elementos del grupo. - items: Vec, - /// Devuelve si el grupo de elementos está deshabilitado. - disabled: bool, -} - -impl Group { - /// Crea un nuevo grupo con la etiqueta indicada. - pub fn new(label: L10n) -> Self { - Self { - label, - ..Self::default() - } - } - - // **< Group BUILDER >************************************************************************** - - /// Añade un elemento al grupo. Los elementos se muestran en el orden en que se añaden. - pub fn with_item(mut self, item: Item) -> Self { - self.items.push(item); - self - } - - /// Establece si el grupo de elementos está deshabilitado en bloque. - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -// **< Entry >************************************************************************************** - -/// Entrada de [`form::select::Field`] con un elemento o un grupo de elementos. -/// -/// Cada entrada se crea implícitamente cuando se usa [`form::select::Field::with_item()`] para -/// añadir un elemento individual o [`form::select::Field::with_group()`] para añadir un grupo de -/// elementos a una lista de selección. -/// -/// Con [`form::select::Field::entries()`] se pueden recuperar todas las entradas para su -/// renderizado. -#[derive(Clone, Debug)] -pub enum Entry { - /// Elemento individual. - Item(Item), - /// Grupo de elementos. - Group(Group), -} - -// **< Field >************************************************************************************** - -/// Componente para crear una **lista de selección**. -/// -/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir -/// uno, o más de uno si se activa la selección múltiple con -/// [`with_multiple()`](Self::with_multiple). -/// -/// Los elementos individuales se añaden con [`with_item()`](Self::with_item); los grupos de -/// elementos con un encabezado común se añaden con [`with_group()`](Self::with_group). Ambos -/// métodos pueden combinarse libremente. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let idioma = form::select::Field::new() +/// let language = form::select::Field::new() /// .with_name("language") /// .with_label(L10n::n("Language")) +/// .with_floating_label(true) /// .with_item(form::select::Item::new("", L10n::n("— Choose —")).with_selected(true)) -/// .with_group( -/// form::select::Group::new(L10n::n("Europe")) -/// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) -/// .with_item(form::select::Item::new("fr", L10n::n("French"))), -/// ) -/// .with_group( -/// form::select::Group::new(L10n::n("Americas")) -/// .with_item(form::select::Item::new("en", L10n::n("English"))) -/// .with_item(form::select::Item::new("pt", L10n::n("Portuguese"))), -/// ) -/// .with_required(true); +/// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) +/// .with_item(form::select::Item::new("en", L10n::n("English"))); /// ``` -/// -/// Cuando el usuario selecciona un elemento y envía el formulario, el navegador transmite -/// `name=valor`. Si el campo es obligatorio el valor siempre estará presente y puede deserializarse -/// como `String`; si es opcional, usa `Option`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// language: String, // Siempre presente (campo obligatorio). -/// // language: Option, // None si no se selecciona ninguna opción. -/// } -/// ``` -/// -/// Con selección múltiple activa, el navegador envía un valor por cada elemento marcado; si no se -/// marca ninguno, no envía nada. Usa `Vec` con `#[serde(default)]`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// #[serde(default)] -/// interests: Vec, // p. ej. ["art", "tech"] o [] si no se marcó ninguna. -/// } -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor de la lista de selección. - classes: Classes, - /// Devuelve el nombre del campo. - name: AttrName, - /// Devuelve la etiqueta del campo. - label: Attr, - /// Devuelve si la etiqueta se muestra flotante sobre el campo. - floating_label: bool, - /// Devuelve el texto de ayuda del campo. - help_text: Attr, - /// Devuelve las entradas de la lista (elementos individuales y grupos de elementos). - entries: Vec, - /// Devuelve si la lista permite selección múltiple. - multiple: bool, - /// Devuelve el número de filas visibles de la lista de selección. - rows: Attr, - /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, - /// Devuelve si la lista recibe el foco automáticamente al cargar la página. - autofocus: bool, - /// Devuelve si la selección de un elemento es obligatoria. - required: bool, - /// Devuelve si la lista está deshabilitada. - disabled: bool, -} - -impl Component for Field { - fn new() -> Self { - Self::default() - } - - fn id(&self) -> Option { - self.id.get() - } - - fn setup(&mut self, _cx: &Context) { - if *self.floating_label() { - self.multiple = false; - self.rows.alter_opt(None::); - self.alter_classes(ClassesOp::Prepend, "form-floating"); - } - self.alter_classes(ClassesOp::Prepend, "form-field form-field-select"); - } - - fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); - let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); - let label = match self.label().lookup(cx) { - Some(text) => html! { - label for=[select_id.as_deref()] class="form-label" { - (text) - @if *self.required() { - span - class="form-required" - title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div id=[container_id.as_deref()] class=[self.classes().get()] { - @if !*self.floating_label() { - (label) - } - select - id=[select_id.as_deref()] - class="form-select" - name=[self.name().get()] - multiple[*self.multiple()] - size=[self.rows().get()] - autocomplete=[self.autocomplete().get()] - autofocus[*self.autofocus()] - required[*self.required()] - disabled[*self.disabled()] - { - @for entry in self.entries() { - @match entry { - Entry::Item(opt) => { - option - value=(opt.value().as_str().unwrap_or("")) - selected[*opt.selected()] - disabled[*opt.disabled()] - { - (opt.label().using(cx)) - } - } - Entry::Group(group) => { - optgroup - label=(group.label().using(cx)) - disabled[*group.disabled()] - { - @for opt in group.items() { - option - value=(opt.value().as_str().unwrap_or("")) - selected[*opt.selected()] - disabled[*opt.disabled()] - { - (opt.label().using(cx)) - } - } - } - } - } - } - } - @if *self.floating_label() { - (label) - } - @if let Some(description) = self.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) - } -} - -impl Field { - // **< Field BUILDER >*************************************************************************** - - /// Establece el identificador único (`id`) del contenedor del campo. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - - /// Modifica la lista de clases CSS aplicadas al contenedor de la lista de selección. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); - self - } - - /// Establece el nombre del campo (atributo `name`). - /// - /// Sin él, el valor seleccionado no se transmite al servidor al enviar el formulario. Para - /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. - #[builder_fn] - pub fn with_name(mut self, name: impl AsRef) -> Self { - self.name.alter_name(name); - self - } - - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). - #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); - self - } - +pub trait SelectBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// /// Cuando está activo, la etiqueta se superpone al control y permanece flotante siempre que /// haya una opción visible. /// - /// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará los - /// valores establecidos con [`with_multiple()`](Self::with_multiple) y - /// [`with_rows()`](Self::with_rows) antes del renderizado. - #[builder_fn] - pub fn with_floating_label(mut self, floating_label: bool) -> Self { - self.floating_label = floating_label; - self - } + /// Si se usa la etiqueta flotante, se anulan los valores establecidos con + /// [`with_multiple()`](form::select::Field::with_multiple) y + /// [`with_rows()`](form::select::Field::with_rows) antes del renderizado. + fn with_floating_label(self, floating: bool) -> Self; +} - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). - #[builder_fn] - pub fn with_help_text(mut self, help_text: impl Into>) -> Self { - self.help_text.alter_opt(help_text.into()); - self - } - - /// Añade un elemento individual a la lista de selección. - /// - /// Los elementos y grupos se muestran en el orden en que se añaden. - #[builder_fn] - pub fn with_item(mut self, item: Item) -> Self { - self.entries.push(Entry::Item(item)); - self - } - - /// Añade un grupo de elementos a la lista de selección. - /// - /// Los elementos y grupos se muestran en el orden en que se añaden. - #[builder_fn] - pub fn with_group(mut self, group: Group) -> Self { - self.entries.push(Entry::Group(group)); - self - } - - /// Establece si el control permite seleccionar varios elementos. - /// - /// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se - /// recomienda combinar con [`with_rows()`](Self::with_rows) para controlar el número de filas - /// visibles. - /// - /// Para un número reducido de elementos con etiquetas descriptivas considera usar - /// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en - /// pantallas pequeñas. - /// - /// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label). - #[builder_fn] - pub fn with_multiple(mut self, multiple: bool) -> Self { - self.multiple = multiple; - self - } - - /// Establece el número de filas visibles de la lista de selección. - /// - /// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de - /// desplegable, tanto en modo simple como múltiple. Con `None` se omite el atributo y presenta - /// el control como desplegable (comportamiento por defecto). - /// - /// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin - /// necesidad de recurrir al desplazamiento. - /// - /// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label). - #[builder_fn] - pub fn with_rows(mut self, rows: Option) -> Self { - self.rows.alter_opt(rows); - self - } - - /// Establece la configuración de autocompletado del campo. - /// - /// Permite al navegador rellenar automáticamente el elemento seleccionado en listas de países - /// (`"country"`), idiomas (`"language"`), sexo (`"sex"`) u otros campos con valores - /// predefinidos. En listas de selección múltiples no es útil en la práctica, ya que los - /// navegadores no gestionan selecciones múltiples con autocompletado. - /// - /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para - /// omitir el atributo. - #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); - self - } - - /// Establece si el campo recibe el foco automáticamente al cargar la página. - #[builder_fn] - pub fn with_autofocus(mut self, autofocus: bool) -> Self { - self.autofocus = autofocus; - self - } - - /// Establece si el campo es obligatorio. - #[builder_fn] - pub fn with_required(mut self, required: bool) -> Self { - self.required = required; - self - } - - /// Establece si el campo está deshabilitado. - #[builder_fn] - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self +impl SelectBootsier for Field { + fn with_floating_label(self, floating: bool) -> Self { + if floating { + self.with_prop(PropsOp::add_classes("form-floating")) + } else { + self.with_prop(PropsOp::remove_classes("form-floating")) + } } } diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index 81b32783..7cf9c045 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -1,290 +1,44 @@ +//! Definiciones para crear áreas de texto en formularios. + use pagetop::prelude::*; -use crate::LOCALES_BOOTSIER; -use crate::theme::form; +pub use pagetop::base::component::form::Textarea; -/// Componente para crear un **área de texto** de formulario. +/// Extensión de Bootsier para [`form::Textarea`]. /// -/// Permite escribir en un área de texto de más de una línea, con una etiqueta opcional y atributos -/// como el número de filas a presentar, longitud mínima (`minlength`) y máxima (`maxlength`), texto -/// indicativo (`placeholder`) o autocompletado (`autocomplete`). +/// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se +/// superpone al control mientras no hay ninguna opción seleccionada y permanece flotante cuando hay +/// una selección activa. /// -/// # Ejemplo +/// ```rust,no_run +/// # use pagetop::locale::L10n; +/// # use pagetop::core::component::Component; +/// use pagetop_bootsier::theme::*; /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let descripcion = form::Textarea::new() -/// .with_name("description") -/// .with_label(L10n::n("Description")) -/// .with_rows(Some(8)) -/// .with_maxlength(Some(500)) +/// let comentario = form::Textarea::new() +/// .with_name("comment") +/// .with_label(L10n::n("Comment")) /// .with_placeholder(L10n::n("Write here...")) -/// .with_required(true); +/// .with_floating_label(true); /// ``` -/// -/// Al enviar el formulario el navegador transmite `name=valor`. Un área de texto siempre envía su -/// valor, incluso si está vacía. En el servidor se deserializa como `String`: -/// -/// ```rust,ignore -/// #[derive(serde::Deserialize)] -/// struct FormData { -/// description: String, // Siempre presente; cadena vacía si el usuario no escribió nada. -/// } -/// ``` -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Textarea { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del área de texto. - classes: Classes, - /// Devuelve el nombre del campo. - name: AttrName, - /// Devuelve el valor inicial del área de texto. - value: AttrValue, - /// Devuelve la etiqueta del campo. - label: Attr, - /// Devuelve si la etiqueta se muestra flotante sobre el campo. - floating_label: bool, - /// Devuelve el texto de ayuda del campo. - help_text: Attr, - /// Devuelve el número de filas visibles del área de texto. - rows: Attr, - /// Devuelve la longitud mínima permitida en caracteres. - minlength: Attr, - /// Devuelve la longitud máxima permitida en caracteres. - maxlength: Attr, - /// Devuelve el texto indicativo del área de texto. - placeholder: Attr, - /// Devuelve la configuración de autocompletado del campo. - autocomplete: Attr, - /// Devuelve si el campo recibe el foco automáticamente al cargar la página. - autofocus: bool, - /// Devuelve si el campo es de sólo lectura. - readonly: bool, - /// Devuelve si el campo es obligatorio. - required: bool, - /// Devuelve si el campo está deshabilitado. - disabled: bool, -} - -impl Component for Textarea { - fn new() -> Self { - Self::default() - } - - fn id(&self) -> Option { - self.id.get() - } - - fn setup(&mut self, _cx: &Context) { - if *self.floating_label() { - self.rows.alter_opt(None::); - self.alter_classes(ClassesOp::Prepend, "form-floating"); - } - self.alter_classes(ClassesOp::Prepend, "form-field form-field-textarea"); - } - - fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); - let textarea_id = container_id - .as_deref() - .map(|id| util::join!(id, "-textarea")); - // La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo - // está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`. - let placeholder = if *self.floating_label() { - Some(self.placeholder().lookup(cx).unwrap_or_default()) - } else { - self.placeholder().lookup(cx) - }; - let label = match self.label().lookup(cx) { - Some(text) => html! { - label for=[textarea_id.as_deref()] class="form-label" { - (text) - @if *self.required() { - span - class="form-required" - title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div id=[container_id.as_deref()] class=[self.classes().get()] { - @if !*self.floating_label() { - (label) - } - textarea - id=[textarea_id.as_deref()] - class="form-control" - name=[self.name().get()] - rows=[self.rows().get()] - minlength=[self.minlength().get()] - maxlength=[self.maxlength().get()] - placeholder=[placeholder] - autocomplete=[self.autocomplete().get()] - autofocus[*self.autofocus()] - readonly[*self.readonly()] - required[*self.required()] - disabled[*self.disabled()] - { - @if let Some(value) = self.value().get() { - (value) - } - } - @if *self.floating_label() { - (label) - } - @if let Some(description) = self.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) - } -} - -impl Textarea { - // **< Textarea BUILDER >*********************************************************************** - - /// Establece el identificador único (`id`) del contenedor del campo. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - - /// Modifica la lista de clases CSS aplicadas al contenedor del campo. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); - self - } - - /// Establece el nombre del campo (atributo `name`). - /// - /// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para - /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. - #[builder_fn] - pub fn with_name(mut self, name: impl AsRef) -> Self { - self.name.alter_name(name); - self - } - - /// Establece el valor inicial del área de texto. - #[builder_fn] - pub fn with_value(mut self, value: impl AsRef) -> Self { - self.value.alter_str(value); - self - } - - /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). - #[builder_fn] - pub fn with_label(mut self, label: impl Into>) -> Self { - self.label.alter_opt(label.into()); - self - } - +pub trait TextareaBootsier { /// Establece si la etiqueta se muestra flotante sobre el campo. /// /// Cuando está activo, la etiqueta se superpone al área de texto y asciende al enfocarlo o - /// cuando tiene contenido. + /// cuando tiene contenido. Requiere que el campo tenga un atributo `placeholder` definido; + /// si no se especifica, se fuerza `placeholder=""` antes del renderizado. /// - /// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará el valor - /// establecido con [`with_rows()`](Self::with_rows) antes del renderizado. Si es necesario, se - /// puede controlar la altura con estilos aplicados al componente. - #[builder_fn] - pub fn with_floating_label(mut self, floating_label: bool) -> Self { - self.floating_label = floating_label; - self - } + /// Si se usa la etiqueta flotante, se anula el valor establecido con + /// [`with_rows()`](form::Textarea::with_rows) antes del renderizado. + fn with_floating_label(self, floating: bool) -> Self; +} - /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). - #[builder_fn] - pub fn with_help_text(mut self, help_text: impl Into>) -> Self { - self.help_text.alter_opt(help_text.into()); - self - } - - /// Establece el número de filas visibles del área de texto. - /// - /// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el - /// estándar. - /// - /// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label). - #[builder_fn] - pub fn with_rows(mut self, rows: Option) -> Self { - self.rows.alter_opt(rows); - self - } - - /// Establece la longitud mínima permitida en caracteres. - #[builder_fn] - pub fn with_minlength(mut self, minlength: Option) -> Self { - self.minlength.alter_opt(minlength); - self - } - - /// Establece la longitud máxima permitida en caracteres. - #[builder_fn] - pub fn with_maxlength(mut self, maxlength: Option) -> Self { - self.maxlength.alter_opt(maxlength); - self - } - - /// Establece o elimina el texto indicativo del área de texto (`None` para quitarlo). - /// - /// Este texto aparece en el área de texto y desaparece en cuanto el usuario empieza a escribir. - /// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo. - #[builder_fn] - pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { - self.placeholder.alter_opt(placeholder.into()); - self - } - - /// Establece la configuración de autocompletado del campo. - /// - /// Permite al navegador sugerir o rellenar automáticamente el contenido del área de texto - /// con valores guardados. Es especialmente útil en áreas con contenido semántico predefinido. - /// - /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para - /// omitir el atributo. - #[builder_fn] - pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { - self.autocomplete.alter_opt(autocomplete); - self - } - - /// Establece si el campo recibe el foco automáticamente al cargar la página. - #[builder_fn] - pub fn with_autofocus(mut self, autofocus: bool) -> Self { - self.autofocus = autofocus; - self - } - - /// Establece si el campo es de sólo lectura. - #[builder_fn] - pub fn with_readonly(mut self, readonly: bool) -> Self { - self.readonly = readonly; - self - } - - /// Establece si el campo es obligatorio. - #[builder_fn] - pub fn with_required(mut self, required: bool) -> Self { - self.required = required; - self - } - - /// Establece si el campo está deshabilitado. - #[builder_fn] - pub fn with_disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self +impl TextareaBootsier for Textarea { + fn with_floating_label(self, floating: bool) -> Self { + if floating { + self.with_prop(PropsOp::add_classes("form-floating")) + } else { + self.with_prop(PropsOp::remove_classes("form-floating")) + } } } diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs index 9ef6aeee..970a6cd4 100644 --- a/extensions/pagetop-bootsier/src/theme/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/icon.rs @@ -15,8 +15,8 @@ pub enum IconKind { #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Icon { - /// Devuelve las clases CSS asociadas al icono. - classes: Classes, + /// Devuelve los atributos HTML y clases CSS del componente. + props: Props, icon_kind: IconKind, aria_label: AttrL10n, } @@ -26,12 +26,16 @@ impl Component for Icon { Self::default() } + fn id(&self) -> Option { + self.props.get_id() + } + fn setup(&mut self, _cx: &Context) { if !matches!(self.icon_kind(), IconKind::None) { - self.alter_classes(ClassesOp::Prepend, "icon"); + self.alter_prop(PropsOp::prepend_classes("icon")); } if let IconKind::Font(font_size) = self.icon_kind() { - self.alter_classes(ClassesOp::Add, font_size.as_str()); + self.alter_prop(PropsOp::add_classes(font_size.as_str())); } } @@ -43,7 +47,7 @@ impl Component for Icon { let has_label = aria_label.is_some(); html! { i - class=[self.classes().get()] + (self.props()) role=[has_label.then_some("img")] aria-label=[aria_label] aria-hidden=[(!has_label).then_some("true")] @@ -60,7 +64,7 @@ impl Component for Icon { viewBox=(viewbox) fill="currentColor" focusable="false" - class=[self.classes().get()] + (self.props()) role=[has_label.then_some("img")] aria-label=[aria_label] aria-hidden=[(!has_label).then_some("true")] @@ -98,10 +102,17 @@ impl Icon { // **< Icon BUILDER >*************************************************************************** - /// Modifica la lista de clases CSS aplicadas al icono. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_value(op, classes); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index df2c28a7..8ab379e7 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -13,10 +13,8 @@ use crate::theme::*; /// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`]. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Image { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas a la imagen. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve las dimensiones de la imagen. size: image::Size, /// Devuelve el origen de la imagen. @@ -31,11 +29,12 @@ impl Component for Image { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, self.source().to_class()); + // Clases CSS por defecto para la imagen, según el origen seleccionado. + self.alter_prop(PropsOp::prepend_classes(self.source().to_class())); } fn prepare(&self, cx: &mut Context) -> Result { @@ -46,8 +45,7 @@ impl Component for Image { image::Source::Logo(logo) => { return Ok(html! { span - id=[self.id()] - class=[self.classes().get()] + (self.props()) style=[dimensions] role=[(!is_decorative).then_some("img")] aria-label=[(!is_decorative).then_some(alt_text)] @@ -65,8 +63,7 @@ impl Component for Image { img src=[source] alt=(alt_text) - id=[self.id()] - class=[self.classes().get()] + (self.props()) style=[dimensions] {} }) } @@ -80,22 +77,22 @@ impl Image { // **< Image BUILDER >************************************************************************** - /// Establece el identificador único (`id`) de la imagen. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas a la imagen. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// /// - Establecer bordes ([`classes::Border`]). /// - Redondear las esquinas ([`classes::Rounded`]). #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index aeb0447e..7f792db3 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -12,7 +12,7 @@ use crate::theme::*; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// use pagetop_bootsier::theme::*; /// @@ -32,10 +32,8 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Nav { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al menú. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el estilo visual seleccionado. nav_kind: nav::Kind, /// Devuelve la distribución y orientación seleccionada. @@ -50,16 +48,17 @@ impl Component for Nav { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, { + // Clases CSS por defecto para el menú, según el estilo y la distribución seleccionados. + self.alter_prop(PropsOp::prepend_classes({ let mut classes = "nav".to_string(); self.nav_kind().push_class(&mut classes); self.nav_layout().push_class(&mut classes); classes - }); + })); } fn prepare(&self, cx: &mut Context) -> Result { @@ -69,7 +68,7 @@ impl Component for Nav { } Ok(html! { - ul id=[self.id()] class=[self.classes().get()] { + ul (self.props()) { (items) } }) @@ -94,17 +93,17 @@ impl Nav { // **< Nav BUILDER >**************************************************************************** - /// Establece el identificador único (`id`) del menú. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al menú. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index 43386baf..9fbbc47d 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -78,10 +78,8 @@ impl ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al elemento. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, } @@ -92,11 +90,11 @@ impl Component for Item { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class()); + self.alter_prop(PropsOp::prepend_classes(self.item_kind().to_class())); } fn prepare(&self, cx: &mut Context) -> Result { @@ -104,7 +102,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { span class="nav-link disabled" aria-disabled="true" { (label.using(cx)) } @@ -137,7 +135,7 @@ impl Component for Item { let aria_disabled = (*disabled).then_some("true"); html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { a class=(classes) href=[href] @@ -153,7 +151,7 @@ impl Component for Item { } ItemKind::Html(html) => html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { (html.render(cx)) } }, @@ -170,7 +168,7 @@ impl Component for Item { .unwrap_or_else(|| "Dropdown".to_string()) }); html! { - li id=[self.id()] class=[self.classes().get()] { + li (self.props()) { a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" @@ -283,17 +281,17 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del elemento. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al elemento. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index 9e5082b6..511ea805 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -13,8 +13,6 @@ use crate::theme::*; /// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Brand { - #[getters(skip)] - id: AttrId, /// Devuelve la imagen de marca (si la hay). image: Embed, /// Devuelve el título de la identidad de marca. @@ -32,10 +30,6 @@ impl Component for Brand { Self::default() } - fn id(&self) -> Option { - self.id.get() - } - fn prepare(&self, cx: &mut Context) -> Result { let image = self.image().render(cx); let title = self.title().using(cx); @@ -56,13 +50,6 @@ impl Component for Brand { impl Brand { // **< Brand BUILDER >************************************************************************** - /// Establece el identificador único (`id`) de la marca. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); - self - } - /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. #[builder_fn] pub fn with_image(mut self, image: Option) -> Self { diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 096ec87a..8709a857 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -18,7 +18,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra **simple**, sólo con un menú horizontal: /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// use pagetop_bootsier::theme::*; /// @@ -33,7 +33,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let navbar = Navbar::simple_toggle() @@ -48,7 +48,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let brand = navbar::Brand::new() @@ -75,7 +75,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let brand = navbar::Brand::new() @@ -93,7 +93,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let oc = Offcanvas::new() @@ -118,7 +118,7 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// /// Barra **fija arriba**: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_bootsier::theme::*; /// let brand = navbar::Brand::new() @@ -136,10 +136,8 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Navbar { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas a la barra de navegación. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el punto de ruptura configurado. expand: BreakPoint, /// Devuelve la disposición configurada para la barra de navegación. @@ -156,16 +154,20 @@ impl Component for Navbar { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, { + fn setup(&mut self, cx: &Context) { + // Asegura que la barra de navegación tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Clases CSS por defecto para la barra de navegación. + self.alter_prop(PropsOp::prepend_classes({ let mut classes = "navbar".to_string(); self.expand().push_class(&mut classes, "navbar-expand", ""); self.position().push_class(&mut classes); classes - }); + })); } fn prepare(&self, cx: &mut Context) -> Result { @@ -198,11 +200,11 @@ impl Component for Navbar { return Ok(html! {}); } - // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas. - let id = cx.required_id::(self.id(), 1); + // `setup()` garantiza que habrá un `id` antes de renderizar. + let id = self.id().unwrap(); Ok(html! { - nav id=(&id) class=[self.classes().get()] { + nav (self.props()) { div class="container-fluid" { @match self.layout() { // Barra más sencilla: sólo contenido. @@ -335,22 +337,22 @@ impl Navbar { // **< Navbar BUILDER >************************************************************************* - /// Establece el identificador único (`id`) de la barra de navegación. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas a la barra de navegación. + /// Modifica identificador, clases CSS o atributos HTML del componente. /// /// También acepta clases predefinidas para: /// /// - Modificar el color de fondo ([`classes::Background`]). /// - Definir la apariencia del texto ([`classes::Text`]). #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/navbar/item.rs b/extensions/pagetop-bootsier/src/theme/navbar/item.rs index 9b48adf1..24cbd026 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/item.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/item.rs @@ -41,7 +41,7 @@ impl Component for Item { fn setup(&mut self, _cx: &Context) { if let Self::Nav(nav) = self { if let Some(mut nav) = nav.get() { - nav.alter_classes(ClassesOp::Prepend, "navbar-nav"); + nav.alter_prop(PropsOp::prepend_classes("navbar-nav")); } } } @@ -57,7 +57,7 @@ impl Component for Item { return Ok(html! {}); } html! { - ul id=[nav.id()] class=[nav.classes().get()] { + ul id=[nav.id()] (nav.props()) { (items) } } diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index a2c014b8..1d9f2c48 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -23,7 +23,7 @@ use crate::theme::*; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// use pagetop_bootsier::theme::*; /// @@ -43,10 +43,8 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Offcanvas { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al panel. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el título del panel. title: L10n, /// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel. @@ -69,17 +67,21 @@ impl Component for Offcanvas { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, { + fn setup(&mut self, cx: &Context) { + // Asegura que el panel tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Clases CSS por defecto para el panel. + self.alter_prop(PropsOp::prepend_classes({ let mut classes = "offcanvas".to_string(); self.breakpoint().push_class(&mut classes, "offcanvas", ""); self.placement().push_class(&mut classes); self.visibility().push_class(&mut classes); classes - }); + })); } fn prepare(&self, cx: &mut Context) -> Result { @@ -90,17 +92,17 @@ impl Component for Offcanvas { impl Offcanvas { // **< Offcanvas BUILDER >********************************************************************** - /// Establece el identificador único (`id`) del panel. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al panel. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } @@ -172,7 +174,8 @@ impl Offcanvas { return html! {}; } - let id = cx.required_id::(self.id(), 1); + // `setup()` garantiza que habrá un `id` antes de renderizar. + let id = self.id().unwrap(); let id_label = util::join!(id, "-label"); let id_target = util::join!("#", id); @@ -191,8 +194,7 @@ impl Offcanvas { html! { div - id=(&id) - class=[self.classes().get()] + (self.props()) tabindex="-1" data-bs-scroll=[body_scroll] data-bs-backdrop=[backdrop] diff --git a/extensions/pagetop-bootsier/static/scss/bootsier.scss b/extensions/pagetop-bootsier/static/scss/bootsier.scss deleted file mode 100644 index 0d52046a..00000000 --- a/extensions/pagetop-bootsier/static/scss/bootsier.scss +++ /dev/null @@ -1,55 +0,0 @@ -@import "bootstrap-5.3.8/mixins/banner"; -@include bsBanner(""); - - -// scss-docs-start import-stack -// Configuration -@import "bootstrap-5.3.8/functions"; -@import "bootstrap-5.3.8/variables"; -@import "bootstrap-5.3.8/variables-dark"; -@import "bootstrap-5.3.8/maps"; -@import "bootstrap-5.3.8/mixins"; -@import "bootstrap-5.3.8/utilities"; - -// Layout & components -@import "bootstrap-5.3.8/root"; -@import "bootstrap-5.3.8/reboot"; -@import "bootstrap-5.3.8/type"; -@import "bootstrap-5.3.8/images"; -@import "bootstrap-5.3.8/containers"; -@import "bootstrap-5.3.8/grid"; -@import "bootstrap-5.3.8/tables"; -@import "bootstrap-5.3.8/forms"; -@import "bootstrap-5.3.8/buttons"; -@import "bootstrap-5.3.8/transitions"; -@import "bootstrap-5.3.8/dropdown"; -@import "bootstrap-5.3.8/button-group"; -@import "bootstrap-5.3.8/nav"; -@import "bootstrap-5.3.8/navbar"; -@import "bootstrap-5.3.8/card"; -@import "bootstrap-5.3.8/accordion"; -@import "bootstrap-5.3.8/breadcrumb"; -@import "bootstrap-5.3.8/pagination"; -@import "bootstrap-5.3.8/badge"; -@import "bootstrap-5.3.8/alert"; -@import "bootstrap-5.3.8/progress"; -@import "bootstrap-5.3.8/list-group"; -@import "bootstrap-5.3.8/close"; -@import "bootstrap-5.3.8/toasts"; -@import "bootstrap-5.3.8/modal"; -@import "bootstrap-5.3.8/tooltip"; -@import "bootstrap-5.3.8/popover"; -@import "bootstrap-5.3.8/carousel"; -@import "bootstrap-5.3.8/spinners"; -@import "bootstrap-5.3.8/offcanvas"; -@import "bootstrap-5.3.8/placeholders"; - -// Helpers -@import "bootstrap-5.3.8/helpers"; - -// Custom definitions -@import "customs"; - -// Utilities -@import "bootstrap-5.3.8/utilities/api"; -// scss-docs-end import-stack diff --git a/extensions/pagetop-htmx/Cargo.toml b/extensions/pagetop-htmx/Cargo.toml new file mode 100644 index 00000000..5a1c254f --- /dev/null +++ b/extensions/pagetop-htmx/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pagetop-htmx" +version = "0.1.0" + +description = """ + Extensión de PageTop que integra HTMX para enriquecer las páginas con interacciones dinámicas. +""" +categories = ["web-programming"] +keywords = ["pagetop", "htmx", "ajax", "ssr"] + +repository.workspace = true +homepage.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +pagetop.workspace = true + +[build-dependencies] +pagetop-build.workspace = true diff --git a/extensions/pagetop-htmx/LICENSE-APACHE b/extensions/pagetop-htmx/LICENSE-APACHE new file mode 100644 index 00000000..56cfd2db --- /dev/null +++ b/extensions/pagetop-htmx/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Manuel Cillero + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/extensions/pagetop-htmx/LICENSE-MIT b/extensions/pagetop-htmx/LICENSE-MIT new file mode 100644 index 00000000..4227455e --- /dev/null +++ b/extensions/pagetop-htmx/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Manuel Cillero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/extensions/pagetop-htmx/README.md b/extensions/pagetop-htmx/README.md new file mode 100644 index 00000000..1bf1c623 --- /dev/null +++ b/extensions/pagetop-htmx/README.md @@ -0,0 +1,88 @@ +
+ +

PageTop HTMX

+ +

Extensión para PageTop que integra HTMX para enriquecer las páginas con interacciones dinámicas.

+ +[![Doc API](https://img.shields.io/docsrs/pagetop-htmx?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-htmx) +[![Crates.io](https://img.shields.io/crates/v/pagetop-htmx.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-htmx) +[![Descargas](https://img.shields.io/crates/d/pagetop-htmx.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-htmx) +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-htmx#licencia) + +
+ +## Sobre PageTop + +[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web +clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y +configurables, basadas en HTML, CSS y JavaScript. + +## Guía rápida + +**Añade la dependencia** a tu `Cargo.toml`: + +```toml +[dependencies] +pagetop-htmx = { ... } +``` + +**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en +`dependencies()` determina la prioridad relativa frente a las otras extensiones: + +```rust +use pagetop::prelude::*; + +struct MyApp; + +impl Extension for MyApp { + fn dependencies(&self) -> Vec { + vec![ + // ... + &pagetop_htmx::Htmx + // ... + ] + } +} +``` + +A partir de ese momento, todas las páginas de la aplicación incluirán automáticamente el script de +HTMX 2. Puedes usar los atributos `hx-*` directamente en tus componentes o el código HTML generado: + +```rust +use pagetop::prelude::*; + +async fn homepage(request: HttpRequest) -> Result { + Page::new(request) + .with_child(Html::with(|_| html! { + button hx-get="/api/hello" hx-target="#result" { + "Say hello" + } + div #result {} + })) + .render() +} +``` + +## Créditos + +Este *crate* integra la biblioteca [HTMX 2.0.10](https://htmx.org), distribuida bajo licencia +[BSD 2-Clause](https://github.com/bigskysoftware/htmx/blob/master/LICENSE). + +## Advertencia + +**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su +ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos +hasta que se libere la versión **1.0.0**. + +## Licencia + +El código está disponible bajo una doble licencia: + + * **Licencia MIT** + ([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT) + + * **Licencia Apache, Versión 2.0** + ([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0) + +Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en +el ecosistema Rust. diff --git a/extensions/pagetop-htmx/assets/js/htmx.min.js b/extensions/pagetop-htmx/assets/js/htmx.min.js new file mode 100644 index 00000000..3b7ac1ac --- /dev/null +++ b/extensions/pagetop-htmx/assets/js/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.10"};Q.onLoad=j;Q.process=Ft;Q.on=ye;Q.off=xe;Q.trigger=ae;Q.ajax=Nn;Q.find=f;Q.findAll=y;Q.closest=g;Q.remove=z;Q.addClass=w;Q.removeClass=b;Q.toggleClass=G;Q.takeClass=W;Q.swap=_e;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=$;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:U,findThisElement:we,filterValues:yn,swap:_e,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:A,getExpressionVars:Rn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Se,makeFragment:P,mergeObjects:le,makeSettleInfo:Sn,oobSwap:He,querySelectorExt:ce,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const R=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function q(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function A(e,t){while(e&&!t(e)){e=c(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;A(t,function(e){return!!(r=o(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function N(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function I(e){if("parseHTMLUnsafe"in Document){return Document.parseHTMLUnsafe(e)}const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function D(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){H(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=N(t);let r;if(n==="html"){r=new DocumentFragment;const i=I(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=I(t);L(r,i.body);r.title=i.title}else{const i=I('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){D(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function M(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){H(e);return null}}function U(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function V(e){try{const t=new URL(e,window.location.href);e=t.pathname+t.search}catch(e){}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function $(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function y(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return y(te(),e)}}function x(){return window}function z(e,t){e=S(e);if(t){x().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function J(e){return e instanceof HTMLElement?e:null}function K(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function w(e,t,n){e=ue(S(e));if(!e){return}if(n){x().setTimeout(function(){w(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function b(e,t,n){let r=ue(S(e));if(!r){return}if(n){x().setTimeout(function(){b(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function G(e,t){e=S(e);e.classList.toggle(t)}function W(e,t){e=S(e);ie(e.parentElement.children,function(e){b(e,t)});w(ue(e),t)}function g(e,t){e=ue(S(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Z(e,t){return e.substring(e.length-t.length)===t}function Y(e){const t=e.trim();if(l(t,"<")&&Z(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=S(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=Y(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),Y(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),Y(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,Y(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=ge(t,Y(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=q(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const u=p(q(t,!!n));i.push(...F(u.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=p(q(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ce(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function S(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function me(e,t,n,r){if(k(t)){return{target:te().body,event:K(e),listener:t,options:n}}else{return{target:S(e),event:K(t),listener:n,options:r}}}function ye(t,n,r,o){Gn(function(){const e=me(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function xe(t,n,r){Gn(function(){const e=me(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const be=te().createElement("output");function ve(t,n){const e=ne(t,n);if(e){if(e==="this"){return[we(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ue(A(t,function(e){return e!==t&&s(ue(e),n)}));if(i){r.push(...ve(i,n))}}if(r.length===0){H('The selector "'+e+'" on '+n+" returned no matches!");return[be]}else{return r}}}}function we(e,t){return ue(A(e,function(e){return a(ue(e),t)!=null}))}function Se(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return we(e,"hx-target")}else{return ce(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ee(e){return Q.config.attributesToSettle.includes(e)}function Ce(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ee(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ee(e.name)){t.setAttribute(e.name,e.value)}})}function Oe(t,e){const n=Jn(e);for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Oe(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){Re(t);je(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o,target:n})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function Re(e){ie(y(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","
");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function qe(i,e,s){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const e=p(i);const r=e&&e.querySelector(CSS.escape(t.tagName)+"#"+CSS.escape(n));if(r&&r!==e){const o=t.cloneNode();Ce(t,r);s.tasks.push(function(){Ce(t,o)})}}})}function Ae(e){return function(){b(e,Q.config.addedClass);Ft(ue(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=J(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){qe(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;w(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function _e(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=S(h);const r=g.contextElement?q(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t0){x().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){x().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function ze(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(M(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,C)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,Ze);const l=o.length;const u=O(o,/[,\[\s]/);if(u!==""){if(u==="every"){const c={trigger:"every"};O(o,Ze);c.pollInterval=d(O(o,/[,\[\s]/));O(o,Ze);var i=nt(e,o,"event");if(i){c.eventFilter=i}r.push(c)}else{const f={trigger:u};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,Ze);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,C))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,C);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,C))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,C)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,C)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,Ze)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,Ze)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ut(e,t,n){const r=oe(e);r.timeout=x().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ut(e,t,n)}},n.pollInterval)}function ct(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ct(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ue(e);if(ft(n)){E(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,u,e,c,f){const a=oe(l);let t;if(c.from){t=m(l,c.from)}else{t=[l]}if(c.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(c)){a.lastValue.set(c,new WeakMap)}a.lastValue.get(c).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(c.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(c,l,e)){return}const t=oe(e);t.triggerSpec=c;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(c.consume){e.stopPropagation()}if(c.target&&e.target){if(!h(ue(e.target),c.target)){return}}if(c.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(c.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(c);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(c.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");u(l,e);a.throttle=x().setTimeout(function(){a.throttle=null},c.throttle)}}else if(c.delay>0){a.delayed=x().setTimeout(function(){ae(l,"htmx:trigger");u(l,e)},c.delay)}else{ae(l,"htmx:trigger");u(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:c.trigger,listener:s,on:i});i.addEventListener(c.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&B(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){x().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(ft(n)){E(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ce(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ut(ue(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Rt(e){const t=At(e.target);const n=It(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=It(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ue(e),"button, input[type='submit']")}function Nt(e){return e.form||g(e,"form")}function It(e){const t=At(e.target);if(!t){return}const n=Nt(t);if(!n){return}return oe(n)}function Lt(e){e.addEventListener("click",Rt);e.addEventListener("focusin",Rt);e.addEventListener("focusout",qt)}function Dt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){De(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!U()){return null}t=V(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);_e(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){_e(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=ve(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;w(e,Q.config.requestClass)});return t}function nn(e){let t=ve(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;if(!e.hasAttribute("disabled")){e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")}});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){b(e,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0&&e.hasAttribute("data-disabled-by-htmx")){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function cn(e){if(e instanceof HTMLSelectElement&&e.multiple){return F(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return F(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,cn(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){un(e.name,cn(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Nt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const c=s.lastButtonClicked||e;const f=ee(c,"name");ln(f,c.value,o)}const u=ve(e,"hx-include");ie(u,function(e){fn(n,r,i,ue(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Dn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=c;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{H("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Dn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ce(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){x().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ce(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const u in n){if(n.hasOwnProperty(u)){if(i[u]==null){i[u]=n[u]}}}}return Cn(ue(c(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Hn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Tn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Rn(e,t){return le(Hn(e,t),Tn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function T(e,t){return t.test(e.getAllResponseHeaders())}function Nn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:S(r)||be,returnPromise:true})}else{let e=S(r.target);if(r.target&&!e||r.source&&!e&&!S(r.source)){e=be}return he(t,n,S(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true,push:r.push,replace:r.replace,selectOOB:r.selectOOB})}}else{return he(t,n,null,null,{returnPromise:true})}}function In(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Ln(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Dn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Pn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Pn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||Vn;const F=i.select||null;if(!se(r)){re(s);return e}const u=i.targetOverride||ue(Se(r));if(u==null||u==be){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let c=oe(r);const f=c.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const N=ee(f,"formmethod");if(N!=null){if(de.includes(N.toLowerCase())){t=N}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:u,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let B=false;if(d){const I=d.split(":");const L=I[0].trim();if(L==="this"){h=we(r,"hx-sync")}else{h=ue(ce(r,L))}d=(I[1]||"drop").trim();c=oe(h);if(d==="drop"&&c.xhr&&c.abortable!==true){re(s);return e}else if(d==="abort"){if(c.xhr){re(s);return e}else{B=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(c.xhr){if(c.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const D=oe(o);if(D&&D.triggerSpec&&D.triggerSpec.queue){p=D.triggerSpec.queue}}if(p==null){p="last"}}if(c.queuedRequests==null){c.queuedRequests=[]}if(p==="first"&&c.queuedRequests.length===0){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){c.queuedRequests=[];c.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;c.xhr=g;c.abortable=B;const m=function(){c.xhr=null;c.abortable=false;if(c.queuedRequests!=null&&c.queuedRequests.length>0){const e=c.queuedRequests.shift();e()}};const X=ne(r,"hx-prompt");if(X){var y=prompt(X);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:u})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,u,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Dn(i.values))}const j=Dn(Rn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(u,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:u,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Dn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let H=n;if(E){H=z;const Z=!w.keys().next().done;if(Z){if(H.indexOf("?")<0){H+="?"}else{H+="&"}H+=gn(w);if(O){H+="#"+O}}}if(!Ln(r,H,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),H,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const P in x){if(x.hasOwnProperty(P)){const Y=x[P];qn(g,P,Y)}}}const T={xhr:g,target:u,requestConfig:C,etc:i,boosted:$,select:F,pathInfo:{requestPath:n,finalRequestPath:H,responsePath:null,anchor:O}};g.onload=function(){try{const t=In(r);T.pathInfo.responsePath=An(g);M(r,T);if(T.keepIndicators!==true){rn(R,q)}ae(r,"htmx:afterRequest",T);ae(r,"htmx:afterOnLoad",T);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",T);ae(e,"htmx:afterOnLoad",T)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},T));throw e}finally{m()}};g.onerror=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendError",T);re(l);m()};g.onabort=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendAbort",T);re(l);m()};g.ontimeout=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:timeout",T);re(l);m()};if(!ae(r,"htmx:beforeRequest",T)){re(s);m();return e}var R=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",T);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(T(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(T(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(T(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;let l=t.etc.push||ne(e,"hx-push-url");let u=t.etc.replace||ne(e,"hx-replace-url");if(l==="false")l=null;if(u==="false")u=null;const c=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(u){f="replace";a=u}else if(c){f="push";a=s||i}if(a){if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Bn(e){for(var t=0;t`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.detail.elt||e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};x().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/extensions/pagetop-htmx/build.rs b/extensions/pagetop-htmx/build.rs new file mode 100644 index 00000000..bbf6de4b --- /dev/null +++ b/extensions/pagetop-htmx/build.rs @@ -0,0 +1,13 @@ +use pagetop_build::{StaticFilesBundle, copy_dir}; + +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"); + + copy_dir("assets", "static")?; + + StaticFilesBundle::from_dir("./static", None) + .with_name("htmx") + .build() +} diff --git a/extensions/pagetop-htmx/src/hx.rs b/extensions/pagetop-htmx/src/hx.rs new file mode 100644 index 00000000..f1f1f666 --- /dev/null +++ b/extensions/pagetop-htmx/src/hx.rs @@ -0,0 +1,549 @@ +//! Constantes para los atributos y valores de HTMX 2. +//! +//! Usar estas constantes en lugar de literales evita errores tipográficos en tiempo de compilación. +//! Una mala declaración como `"hx-gte"` no genera ningún error en tiempo de ejecución, falla +//! silenciosamente. Con constantes, el compilador lo detecta de inmediato. +//! +//! # Atributos estáticos en `html!` +//! +//! Para valores conocidos en tiempo de compilación, los atributos `hx-*` pueden escribirse +//! directamente en la macro `html!` sin necesidad de [`Props`](pagetop::html::Props): +//! +//! ```rust,no_run +//! use pagetop::prelude::*; +//! +//! let markup = html! { +//! button hx-get="/api/items" hx-target="#list" hx-swap="outerHTML" { "Load" } +//! }; +//! ``` +//! +//! # Atributos dinámicos con [`Props`](pagetop::html::Props) +//! +//! Cuando los valores se construyen en tiempo de ejecución o se inyectan desde una extensión, +//! puedes usar [`Props`](pagetop::html::Props) combinado con las constantes de este módulo: +//! +//! ```rust,no_run +//! use pagetop::prelude::*; +//! use pagetop_htmx::hx; +//! +//! let endpoint = "/api/items"; // Calculado en tiempo de ejecución. +//! +//! let props = Props::new(hx::GET, endpoint) +//! .with_prop(PropsOp::set(hx::TARGET, "#list")) +//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML)); +//! +//! let markup = html! { +//! button (props) { "Load" } +//! }; +//! ``` +//! +//! # Integración en componentes +//! +//! El patrón recomendado es añadir un campo `props: Props` al componente y exponerlo con +//! `with_prop()`. Cualquier extensión puede entonces inyectar atributos HTMX sin que el componente +//! ni el tema necesiten conocer HTMX: +//! +//! ```rust,no_run +//! use pagetop::prelude::*; +//! use pagetop_htmx::hx; +//! +//! #[derive(AutoDefault, Getters)] +//! pub struct MyButton { +//! props: Props, +//! } +//! +//! impl MyButton { +//! pub fn new() -> Self { Self::default() } +//! +//! #[builder_fn] +//! pub fn with_prop(mut self, op: PropsOp) -> Self { +//! self.props.alter_prop(op); +//! self +//! } +//! } +//! +//! MyButton::new() +//! .with_prop(PropsOp::set(hx::POST, "/api/save")) +//! .with_prop(PropsOp::set(hx::TARGET, "#message")) +//! .with_prop(PropsOp::set(hx::SWAP, hx::swap::INNER_HTML)); +//! ``` +//! +//! # Eventos en línea +//! +//! Para el atributo `hx-on:*` (cuyo nombre incluye el evento y no puede ser una constante) usa las +//! funciones [`on()`] y [`on_htmx()`]: +//! +//! ```rust,no_run +//! use pagetop::prelude::*; +//! use pagetop_htmx::hx; +//! +//! // Evento nativo del DOM: hx-on:click="..." +//! // Evento propio de HTMX: hx-on::after-swap="..." +//! let props = Props::new(hx::on("click"), "this.classList.toggle('active')") +//! .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "console.log('done')")); +//! ``` + +// **< HTTP Methods >******************************************************************************* + +/// Realiza una petición GET al servidor y aplica la respuesta al objetivo. +/// +/// Es el atributo HTMX más común: carga contenido desde el servidor sin recargar la página. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// let props = Props::new(hx::GET, "/api/search") +/// .with_prop(PropsOp::set(hx::TARGET, "#results")); +/// ``` +pub const GET: &str = "hx-get"; + +/// Realiza una petición POST al servidor. +/// +/// Se usa habitualmente para enviar datos de formulario o acciones que modifican el estado del +/// servidor. Para subida de ficheros, combinar con [`ENCODING`] = `"multipart/form-data"`. +pub const POST: &str = "hx-post"; + +/// Realiza una petición PUT al servidor. +pub const PUT: &str = "hx-put"; + +/// Realiza una petición PATCH al servidor. +pub const PATCH: &str = "hx-patch"; + +/// Realiza una petición DELETE al servidor. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// // Al eliminar un elemento, reemplazarlo con respuesta vacía borra el nodo del DOM. +/// let props = Props::new(hx::DELETE, "/api/item/42") +/// .with_prop(PropsOp::set(hx::TARGET, "closest li")) +/// .with_prop(PropsOp::set(hx::SWAP, hx::swap::OUTER_HTML)); +/// ``` +pub const DELETE: &str = "hx-delete"; + +// **< Target and Swap >**************************************************************************** + +/// Selector CSS del elemento que recibirá la respuesta. Por defecto, el elemento mismo. +/// +/// Además de selectores CSS estándar, HTMX acepta `"this"` (el elemento), `"closest X"` (ancestro +/// más próximo), `"find X"` (descendiente) y `"next X"` / `"previous X"`. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// let props = Props::new(hx::GET, "/api/detalles") +/// .with_prop(PropsOp::set(hx::TARGET, "closest article")); +/// ``` +pub const TARGET: &str = "hx-target"; + +/// Cómo se inserta la respuesta en el DOM. Por defecto, `innerHTML`. +/// +/// Los valores estándar están disponibles en el sub-módulo [`swap`]. Además del modo base, se +/// pueden añadir modificadores separados por espacio: retrasos (`swap:200ms`), tiempo de +/// asentamiento (`settle:300ms`), scroll (`scroll:top`) y foco (`focus-scroll:true`). +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// // Reemplaza el elemento completo con una transición de 300 ms. +/// let props = Props::new(hx::SWAP, "outerHTML swap:300ms"); +/// // O usando la constante tipada más los modificadores: +/// let props = Props::new(hx::SWAP, format!("{} swap:300ms", hx::swap::OUTER_HTML)); +/// ``` +pub const SWAP: &str = "hx-swap"; + +/// Sustituye fuera de banda el elemento del DOM cuyo `id` coincida con el indicado. +/// +/// La respuesta del servidor puede incluir elementos marcados con `hx-swap-oob="true"`: HTMX los +/// extrae y actualiza el DOM en la posición correcta, independientemente del objetivo principal. +pub const SWAP_OOB: &str = "hx-swap-oob"; + +/// Selector CSS del fragmento de la respuesta que se insertará en el objetivo. +/// +/// Permite devolver una página completa desde el servidor y que HTMX extraiga sólo la parte +/// relevante, facilitando la reutilización de rutas existentes. +pub const SELECT: &str = "hx-select"; + +/// Selector CSS de fragmentos de la respuesta que se sustituyen fuera de banda. +pub const SELECT_OOB: &str = "hx-select-oob"; + +// **< Trigger >************************************************************************************ + +/// Evento que activa la petición. Por defecto, `click` en botones y links, `change` en inputs. +/// +/// Los valores simples están disponibles en el sub-módulo [`trigger`]. Las expresiones de disparo +/// compuestas se escriben como literales de cadena: +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// // Buscar mientras se escribe, con 400 ms de espera y sólo si el valor cambia: +/// let props = Props::new(hx::GET, "/api/search") +/// .with_prop(PropsOp::set(hx::TRIGGER, "keyup changed delay:400ms")) +/// .with_prop(PropsOp::set(hx::TARGET, "#results")); +/// +/// // Disparar una vez al cargar la página: +/// let lazy = Props::new(hx::GET, "/api/stats") +/// .with_prop(PropsOp::set(hx::TRIGGER, "load once")); +/// +/// // Polling cada 5 segundos: +/// let poll = Props::new(hx::GET, "/api/estado") +/// .with_prop(PropsOp::set(hx::TRIGGER, "every 5s")); +/// ``` +pub const TRIGGER: &str = "hx-trigger"; + +/// Convierte en AJAX todos los enlaces y formularios del elemento y sus descendientes. +/// +/// El valor debe ser `"true"` o `"false"`. Cuando vale `"true"`, las respuestas se aplican al +/// `` por defecto; se puede combinar con [`TARGET`] para redirigirlas. +pub const BOOST: &str = "hx-boost"; + +/// Empuja la URL de la respuesta al historial del navegador. +/// +/// Acepta `"true"` (usa la URL de la petición), `"false"` (desactiva) o una URL concreta. Permite +/// navegación con el botón atrás manteniendo el comportamiento SPA. +pub const PUSH_URL: &str = "hx-push-url"; + +/// Reemplaza la URL actual en el historial sin añadir una nueva entrada. +/// +/// Acepta `"true"`, `"false"` o una URL concreta. Útil cuando la petición refina la vista sin que +/// deba ser un paso independiente en el historial. +pub const REPLACE_URL: &str = "hx-replace-url"; + +/// Sincroniza las peticiones del elemento con las de otros elementos. +/// +/// Formato: `"selector:estrategia"`. Estrategias disponibles: `drop` (descarta la nueva si hay una +/// en curso), `abort` (cancela la nueva), `replace` (cancela la anterior), `queue first|last|all` +/// (encola). Ejemplo: `"#form:abort"`. +pub const SYNC: &str = "hx-sync"; + +// **< Request Data >******************************************************************************* + +/// Selector CSS de elementos adicionales cuyos valores se incluyen en la petición. +/// +/// Acepta selectores CSS estándar más las extensiones de HTMX: `"this"`, `"closest X"`, `"find X"`. +/// Útil para incluir campos de un formulario padre en una petición de detalle. +pub const INCLUDE: &str = "hx-include"; + +/// Controla qué parámetros del formulario se envían en la petición. +/// +/// - `"*"` - todos (valor por defecto). +/// - `"none"` - ninguno. +/// - Lista de nombres: `"name surname"` - sólo esos. +/// - Exclusión: `"not surname"` - todos excepto los indicados. +pub const PARAMS: &str = "hx-params"; + +/// Valores extra en JSON que se añaden a los parámetros de la petición. +/// +/// Formato: objeto JSON. Los valores sobreescriben parámetros del formulario con el mismo nombre. +/// Admite JavaScript con el prefijo `js:`: `"js:{date: new Date().toISOString()}"`. +pub const VALS: &str = "hx-vals"; + +/// Cabeceras extra en JSON que se añaden a la petición. +/// +/// Formato: objeto JSON. Permiten enviar contexto (token de sesión, versión de API, etc.) sin +/// exponerlo en los parámetros visibles del formulario. +pub const HEADERS: &str = "hx-headers"; + +/// Codificación de la petición. Por defecto, `"application/x-www-form-urlencoded"`. +/// +/// Usar `"multipart/form-data"` para peticiones que incluyan campos de tipo `file`. +pub const ENCODING: &str = "hx-encoding"; + +// **< Element Behavior >*************************************************************************** + +/// Selector CSS del indicador de carga que se muestra mientras dura la petición. +/// +/// El elemento indicado recibe la clase `htmx-request` durante la petición. Por defecto, si no se +/// especifica, la recibe el propio elemento que realiza la petición. +pub const INDICATOR: &str = "hx-indicator"; + +/// Selector CSS de elementos que se deshabilitan mientras dura la petición. +/// +/// Añade el atributo `disabled` durante la petición y lo elimina al terminar. Evita envíos +/// duplicados al hacer clic varias veces. +pub const DISABLED_ELT: &str = "hx-disabled-elt"; + +/// Muestra un diálogo de confirmación (`window.confirm`) antes de enviar la petición. +/// +/// Si el usuario cancela, la petición no se realiza. El valor es el texto del mensaje. +pub const CONFIRM: &str = "hx-confirm"; + +/// Muestra un `prompt` de texto y envía el resultado como cabecera `HX-Prompt`. +/// +/// Si el usuario cancela el prompt, la petición no se realiza. +pub const PROMPT: &str = "hx-prompt"; + +/// Activa la validación HTML5 del formulario antes de enviar la petición. +/// +/// Si algún campo no supera la validación nativa del navegador, la petición se cancela. +pub const VALIDATE: &str = "hx-validate"; + +/// Preserva el elemento entre respuestas HTMX. +/// +/// El elemento debe tener un `id` único. HTMX no lo destruye ni lo recrea al aplicar la respuesta, +/// manteniendo su estado interno (p. ej. posición de reproducción de un vídeo). +pub const PRESERVE: &str = "hx-preserve"; + +// **< Config and Extensions >********************************************************************** + +/// Activa una o varias extensiones HTMX en el elemento y sus descendientes. +/// +/// Las extensiones se identifican por nombre y se separan con comas. Extensiones comunes: +/// - `"ws"` - soporte WebSocket. +/// - `"sse"` - soporte Server-Sent Events. +/// - `"json-enc"` - codifica la petición como JSON en lugar de form-urlencoded. +/// - `"loading-states"` - gestión avanzada de estados de carga. +pub const EXT: &str = "hx-ext"; + +/// Atributos HTMX que los elementos descendientes NO heredarán de este elemento. +/// +/// Acepta una lista de atributos separados por comas o `"*"` para bloquear toda herencia. +pub const DISINHERIT: &str = "hx-disinherit"; + +/// Atributos HTMX que los elementos descendientes SÍ heredarán (anula [`DISINHERIT`]). +pub const INHERIT: &str = "hx-inherit"; + +/// Opciones de configuración de la petición en JSON. +/// +/// Claves disponibles: `timeout` (ms), `credentials` (`"include"`, `"omit"`...), `noHeaders` +/// (bool), `getWithBody` (bool). +pub const REQUEST: &str = "hx-request"; + +/// Controla si este elemento participa en el historial del navegador. +/// +/// Con el valor `"false"`, las peticiones de este elemento no se guardan en el historial aunque +/// [`PUSH_URL`] esté activo en un elemento padre. +pub const HISTORY: &str = "hx-history"; + +/// Designa el elemento como contenedor del historial del navegador. +/// +/// HTMX guarda y restaura el contenido de este elemento al navegar hacia atrás/adelante. Sólo debe +/// haber un elemento con este atributo en la página. +pub const HISTORY_ELT: &str = "hx-history-elt"; + +/// Desactiva el procesamiento HTMX en el elemento y todos sus descendientes. +/// +/// Útil para aislar zonas del DOM gestionadas por otra librería o para desactivar HTMX en secciones +/// de contenido generado dinámicamente donde no debe intervenir. +pub const DISABLE: &str = "hx-disable"; + +// **< Inline Events (hx-on) >********************************************************************** + +/// Genera `hx-on:{event}` para escuchar eventos nativos del DOM en línea. +/// +/// Es la alternativa de HTMX a los manejadores `on*` de HTML (`onclick`, `onmouseenter`, ...). La +/// diferencia clave está en cómo los trata el navegador bajo una política CSP (*Content Security +/// Policy*): los atributos `on*` son JavaScript en línea y quedan bloqueados si la CSP no incluye +/// `'unsafe-inline'`; en cambio, `hx-on:*` es un atributo de datos que HTMX lee e interpreta desde +/// su propio código ya autorizado, por lo que la CSP no lo bloquea. +/// +/// La CSP puede definirla el servidor en la cabecera HTTP `Content-Security-Policy`, o la +/// aplicación en una etiqueta `` en el `` del +/// documento. En la práctica, pocas aplicaciones configuran una CSP estricta, pero es una buena +/// práctica de seguridad que conviene tener en cuenta. +/// +/// El valor es código JavaScript que se ejecuta cuando el evento se dispara; `event` contiene el +/// objeto del evento. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// let props = Props::new(hx::on("click"), "this.classList.toggle('active')") +/// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'")); +/// ``` +pub fn on(event: &str) -> String { + format!("hx-on:{event}") +} + +/// Genera `hx-on::{event}` para escuchar eventos propios de HTMX en línea. +/// +/// Los eventos de HTMX usan un doble carácter dos-puntos (`hx-on::evento`). El ciclo de vida +/// completo incluye `before-request`, `after-request`, `before-swap`, `after-swap`, +/// `before-settle`, `after-settle`, `after-on-load`, `history-restore`, entre otros. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// let props = Props::new(hx::on_htmx("before-request"), "console.log('enviando...')") +/// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()")); +/// ``` +pub fn on_htmx(event: &str) -> String { + format!("hx-on::{event}") +} + +// **< HTMX Request Headers >*********************************************************************** + +/// Nombres de las cabeceras que HTMX envía con cada petición AJAX. +/// +/// Están en minúsculas porque así las normaliza el módulo `http`. Se pueden usar con +/// [`HttpRequest::headers()`](pagetop::web::HttpRequest::headers) para leer sus valores +/// directamente, aunque lo habitual es usar el trait [`HtmxRequestExt`](crate::HtmxRequestExt). +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_htmx::hx; +/// +/// async fn handler(request: HttpRequest) { +/// if let Some(target) = request.headers().get(hx::request::TARGET) { +/// // El elemento objetivo tenía este id. +/// } +/// } +/// ``` +pub mod request { + /// Siempre `"true"` en peticiones HTMX. Permite distinguirlas de navegaciones directas. + pub const REQUEST: &str = "hx-request"; + /// `"true"` si la petición viene de un enlace o formulario con `hx-boost`. + pub const BOOSTED: &str = "hx-boosted"; + /// URL de la página activa en el navegador cuando se realizó la petición. + pub const CURRENT_URL: &str = "hx-current-url"; + /// `"true"` si la petición es una restauración del historial del navegador. + pub const HISTORY_RESTORE_REQUEST: &str = "hx-history-restore-request"; + /// Texto introducido por el usuario en un diálogo `hx-prompt`. + pub const PROMPT: &str = "hx-prompt"; + /// Valor del atributo `id` del elemento objetivo de la petición. + pub const TARGET: &str = "hx-target"; + /// Valor del atributo `id` del elemento que disparó la petición. + pub const TRIGGER: &str = "hx-trigger"; + /// Valor del atributo `name` del elemento que disparó la petición. + pub const TRIGGER_NAME: &str = "hx-trigger-name"; +} + +// **< HTMX Response Headers >********************************************************************** + +/// Cabeceras de respuesta HTTP para HTMX. +/// +/// Se pueden usar con [`HeaderMap`](pagetop::web::http::HeaderMap) para construir respuestas +/// manualmente, aunque lo habitual es usar el constructor [`HtmxResponse`](crate::HtmxResponse). +/// +/// ```rust,no_run +/// use pagetop_htmx::hx; +/// use pagetop::web::http::{HeaderMap, HeaderName, HeaderValue}; +/// +/// let mut headers = HeaderMap::new(); +/// headers.insert( +/// hx::response::TRIGGER.parse::().unwrap(), +/// HeaderValue::from_static("itemAdded"), +/// ); +/// ``` +pub mod response { + /// Redirige mediante AJAX a la URL o configuración JSON indicada. Ver + /// [`HtmxResponse::location()`](crate::HtmxResponse::location). + pub const LOCATION: &str = "HX-Location"; + /// Empuja la URL indicada al historial del navegador. Ver + /// [`HtmxResponse::push_url()`](crate::HtmxResponse::push_url). + pub const PUSH_URL: &str = "HX-Push-Url"; + /// Provoca una redirección completa del navegador. Ver + /// [`HtmxResponse::redirect()`](crate::HtmxResponse::redirect). + pub const REDIRECT: &str = "HX-Redirect"; + /// Provoca una recarga completa de la página. Ver + /// [`HtmxResponse::refresh()`](crate::HtmxResponse::refresh). + pub const REFRESH: &str = "HX-Refresh"; + /// Reemplaza la URL actual en el historial. Ver + /// [`HtmxResponse::replace_url()`](crate::HtmxResponse::replace_url). + pub const REPLACE_URL: &str = "HX-Replace-Url"; + /// Anula el `hx-swap` del elemento. Ver + /// [`HtmxResponse::reswap()`](crate::HtmxResponse::reswap). + pub const RESWAP: &str = "HX-Reswap"; + /// Anula el `hx-target` del elemento. Ver + /// [`HtmxResponse::retarget()`](crate::HtmxResponse::retarget). + pub const RETARGET: &str = "HX-Retarget"; + /// Anula el `hx-select` del elemento. Ver + /// [`HtmxResponse::reselect()`](crate::HtmxResponse::reselect). + pub const RESELECT: &str = "HX-Reselect"; + /// Dispara eventos JavaScript al completar la respuesta. Ver + /// [`HtmxResponse::trigger()`](crate::HtmxResponse::trigger). + pub const TRIGGER: &str = "HX-Trigger"; + /// Dispara eventos tras la fase *settle*. Ver + /// [`HtmxResponse::trigger_after_settle()`](crate::HtmxResponse::trigger_after_settle). + pub const TRIGGER_AFTER_SETTLE: &str = "HX-Trigger-After-Settle"; + /// Dispara eventos tras el *swap*. Ver + /// [`HtmxResponse::trigger_after_swap()`](crate::HtmxResponse::trigger_after_swap). + pub const TRIGGER_AFTER_SWAP: &str = "HX-Trigger-After-Swap"; +} + +// **< hx-swap Values >***************************************************************************** + +/// Valores estándar del atributo [`SWAP`] (`hx-swap`). +/// +/// Se pueden combinar con modificadores separados por espacio: +/// - `swap:Xms` - tiempo de espera antes de realizar el intercambio. +/// - `settle:Xms` - tiempo de espera antes de quitar las clases de transición. +/// - `scroll:top` / `scroll:bottom` - desplaza el objetivo tras el intercambio. +/// - `show:top` / `show:bottom` - hace visible el objetivo tras el intercambio. +/// - `focus-scroll:true` - sigue al elemento enfocado. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// // Reemplaza el elemento con una transición de 200 ms y desplaza al inicio: +/// let props = Props::new(hx::SWAP, format!("{} swap:200ms scroll:top", hx::swap::OUTER_HTML)); +/// ``` +pub mod swap { + /// Reemplaza el contenido interior del objetivo (valor por defecto de HTMX). + pub const INNER_HTML: &str = "innerHTML"; + /// Reemplaza el elemento objetivo completo. + pub const OUTER_HTML: &str = "outerHTML"; + /// Inserta la respuesta antes de la etiqueta de apertura del objetivo. + pub const BEFORE_BEGIN: &str = "beforebegin"; + /// Inserta la respuesta al inicio del contenido del objetivo. + pub const AFTER_BEGIN: &str = "afterbegin"; + /// Inserta la respuesta al final del contenido del objetivo. + pub const BEFORE_END: &str = "beforeend"; + /// Inserta la respuesta después de la etiqueta de cierre del objetivo. + pub const AFTER_END: &str = "afterend"; + /// Elimina el elemento objetivo independientemente de la respuesta. + pub const DELETE: &str = "delete"; + /// No realiza ningún intercambio; útil cuando sólo importan las cabeceras de respuesta. + pub const NONE: &str = "none"; +} + +// **< hx-trigger Values >************************************************************************** + +/// Eventos comunes del atributo [`TRIGGER`] (`hx-trigger`). +/// +/// Estos valores cubren los disparadores más simples. Las expresiones de disparo compuestas deben +/// escribirse como literales de cadena. Modificadores disponibles: +/// - `once` - se dispara sólo la primera vez. +/// - `changed` - sólo si el valor del elemento ha cambiado. +/// - `delay:Xms` - espera antes de disparar (se cancela si el evento vuelve a ocurrir). +/// - `throttle:Xms` - limita la frecuencia máxima de disparo. +/// - `from:selector` - escucha el evento en otro elemento. +/// - `target:selector` - sólo si el evento viene del selector indicado. +/// - `consume` - evita que el evento se propague a otros elementos HTMX. +/// - `queue:first|last|all|none` - política de cola cuando llegan eventos consecutivos. +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// # use pagetop_htmx::hx; +/// // Búsqueda progresiva: petición 400 ms después de que el usuario deje de escribir. +/// let search = Props::new(hx::TRIGGER, "keyup changed delay:400ms"); +/// +/// // Carga diferida al entrar en el viewport, una sola vez. +/// let lazy = Props::new(hx::TRIGGER, "intersect once"); +/// +/// // Polling: actualiza cada 10 segundos mientras el elemento esté en el DOM. +/// let poll = Props::new(hx::TRIGGER, "every 10s"); +/// +/// // Múltiples eventos: clic o pulsación de Enter en el campo. +/// let multi = Props::new(hx::TRIGGER, "click, keyup[key=='Enter']"); +/// +/// // Escucha un evento personalizado emitido desde otro elemento. +/// let custom = Props::new(hx::TRIGGER, "itemAdded from:body"); +/// ``` +pub mod trigger { + /// Se dispara al hacer clic (valor por defecto en la mayoría de elementos interactivos). + pub const CLICK: &str = "click"; + /// Se dispara cuando el valor del elemento cambia (valor por defecto en `input`/`select`). + pub const CHANGE: &str = "change"; + /// Se dispara al enviar un formulario. + pub const SUBMIT: &str = "submit"; + /// Se dispara al soltar una tecla. + pub const KEYUP: &str = "keyup"; + /// Se dispara cuando la página termina de cargarse. + pub const LOAD: &str = "load"; + /// Se dispara cuando el elemento entra en el área visible del *viewport* al hacer scroll. + pub const REVEALED: &str = "revealed"; + /// Se dispara cuando el elemento intersecta con el *viewport* (Intersection Observer API). + pub const INTERSECT: &str = "intersect"; +} diff --git a/extensions/pagetop-htmx/src/lib.rs b/extensions/pagetop-htmx/src/lib.rs new file mode 100644 index 00000000..e65f310c --- /dev/null +++ b/extensions/pagetop-htmx/src/lib.rs @@ -0,0 +1,129 @@ +/*! +
+ +

PageTop HTMX

+ +

Extensión para PageTop que integra HTMX para enriquecer las páginas con interacciones dinámicas.

+ +[![Doc API](https://img.shields.io/docsrs/pagetop-htmx?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-htmx) +[![Crates.io](https://img.shields.io/crates/v/pagetop-htmx.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-htmx) +[![Descargas](https://img.shields.io/crates/d/pagetop-htmx.svg?label=Descargas&style=for-the-badge&logo=transmission)](https://crates.io/crates/pagetop-htmx) +[![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-htmx#licencia) + +
+ +## Sobre PageTop + +[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web +clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y +configurables, basadas en HTML, CSS y JavaScript. + +## Guía rápida + +**Añade la dependencia** a tu `Cargo.toml`: + +```toml +[dependencies] +pagetop-htmx = { ... } +``` + +**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en +`dependencies()` determina la prioridad relativa frente a las otras extensiones: + +```rust +use pagetop::prelude::*; + +struct MyApp; + +impl Extension for MyApp { + fn dependencies(&self) -> Vec { + vec![ + // ... + &pagetop_htmx::Htmx + // ... + ] + } +} +``` + +A partir de ese momento, todas las páginas de la aplicación incluirán automáticamente el script de +HTMX 2. Puedes usar los atributos `hx-*` directamente en tus componentes o el código HTML generado: + +```rust +use pagetop::prelude::*; + +async fn homepage(request: HttpRequest) -> Result { + Page::new(request) + .with_child(Html::with(|_| html! { + button hx-get="/api/hello" hx-target="#result" { + "Say hello" + } + div #result {} + })) + .render() +} +``` +*/ + +use pagetop::prelude::*; + +pub mod hx; + +mod request; +pub use request::HtmxRequestExt; + +mod response; +pub use response::HtmxResponse; + +include_locales!(LOCALES_HTMX); + +/// Integra HTMX 2 en cualquier aplicación PageTop. +/// +/// Poner esta extensión en [`dependencies()`](pagetop::core::extension::Extension::dependencies) +/// hace que todas las páginas de la aplicación incluyan automáticamente el script de HTMX mediante +/// un atributo [`defer`](pagetop::html::JavaScript::defer). No es necesaria ninguna configuración +/// adicional. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// struct MyApp; +/// +/// impl Extension for MyApp { +/// fn dependencies(&self) -> Vec { +/// vec![ +/// // ... +/// &pagetop_htmx::Htmx +/// // ... +/// ] +/// } +/// } +/// ``` +pub struct Htmx; + +impl Extension for Htmx { + fn name(&self) -> L10n { + L10n::t("extension_name", &LOCALES_HTMX) + } + + fn description(&self) -> L10n { + L10n::t("extension_description", &LOCALES_HTMX) + } + + fn actions(&self) -> Vec { + actions![action::page::BeforeRenderBody::new(add_htmx_script)] + } + + fn configure_router(&self, router: Router) -> Router { + serve_static_files!(router, [htmx] => "/htmx"); + router + } +} + +fn add_htmx_script(page: &mut Page) { + page.alter_assets(AssetsOp::AddJavaScript( + JavaScript::defer("/htmx/js/htmx.min.js").with_version("2.0.10"), + )); +} diff --git a/extensions/pagetop-htmx/src/locale/en-US/extension.ftl b/extensions/pagetop-htmx/src/locale/en-US/extension.ftl new file mode 100644 index 00000000..c6580f02 --- /dev/null +++ b/extensions/pagetop-htmx/src/locale/en-US/extension.ftl @@ -0,0 +1,2 @@ +extension_name = HTMX +extension_description = Integrates HTMX to enrich pages with dynamic interactions. diff --git a/extensions/pagetop-htmx/src/locale/es-ES/extension.ftl b/extensions/pagetop-htmx/src/locale/es-ES/extension.ftl new file mode 100644 index 00000000..1a32e44a --- /dev/null +++ b/extensions/pagetop-htmx/src/locale/es-ES/extension.ftl @@ -0,0 +1,2 @@ +extension_name = HTMX +extension_description = Integra HTMX para enriquecer las páginas con interacciones dinámicas. \ No newline at end of file diff --git a/extensions/pagetop-htmx/src/request.rs b/extensions/pagetop-htmx/src/request.rs new file mode 100644 index 00000000..32b68f86 --- /dev/null +++ b/extensions/pagetop-htmx/src/request.rs @@ -0,0 +1,132 @@ +//! Implementación de [`HtmxRequestExt`] para [`pagetop::web::HttpRequest`]. + +use pagetop::prelude::*; + +// **< HtmxRequestExt >***************************************************************************** + +/// Extiende [`HttpRequest`](pagetop::web::HttpRequest) con métodos para detectar y leer peticiones +/// HTMX. +/// +/// HTMX añade cabeceras especiales a cada petición AJAX. Este trait permite acceder a ellas de +/// forma expresiva, sin manipular [`pagetop::web::http::HeaderMap`] directamente. +/// +/// El patrón más común es devolver una respuesta distinta según si la petición viene de HTMX +/// (fragmento parcial) o de una navegación directa (página completa). Cuando las dos ramas retornan +/// tipos distintos, se usa [`pagetop::web::Response`] como tipo común: +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_htmx::{HtmxRequestExt, HtmxResponse}; +/// +/// async fn list_items(request: HttpRequest) -> Response { +/// if request.is_htmx() { +/// // Fragmento parcial con cabeceras HTMX opcionales. +/// HtmxResponse::new(html! { ul { li { "Item 1" } li { "Item 2" } } }) +/// .into_response() +/// } else { +/// // Página completa para navegación directa. +/// Page::new(request) +/// .with_child(Html::with(|_| html! { +/// ul { li { "Item 1" } li { "Item 2" } } +/// })) +/// .render() +/// .into_response() +/// } +/// } +/// ``` +/// +/// Los nombres de cabecera como constantes están en [`crate::hx::request`]. +pub trait HtmxRequestExt { + /// Devuelve `true` si la petición proviene de HTMX (`HX-Request: true`). + /// + /// Es la comprobación principal para distinguir una petición HTMX de una navegación directa del + /// navegador. + fn is_htmx(&self) -> bool; + + /// Devuelve `true` si la petición proviene de un enlace o formulario con `hx-boost`. + /// + /// Las peticiones boosted son HTMX pero conservan la semántica de navegación completa: el + /// objetivo por defecto es ``. Puede ser útil para no devolver un fragmento parcial en + /// este caso. + fn is_boosted(&self) -> bool; + + /// Devuelve `true` si la petición es una restauración del historial del navegador. + /// + /// Ocurre cuando el usuario navega hacia atrás o adelante y HTMX necesita restaurar el estado + /// de la página. En este caso conviene devolver la página completa. + fn is_history_restore(&self) -> bool; + + /// URL de la página activa en el navegador en el momento de la petición (`HX-Current-URL`). + /// + /// Útil para redirigir o actualizar la URL del historial en función de dónde estaba el usuario. + fn hx_current_url(&self) -> Option<&str>; + + /// Valor del atributo `id` del elemento objetivo de la petición (`HX-Target`). + /// + /// Si el elemento objetivo no tiene `id`, esta cabecera no se envía. + fn hx_target(&self) -> Option<&str>; + + /// Valor del atributo `id` del elemento que disparó la petición (`HX-Trigger`). + /// + /// Si el elemento disparador no tiene `id`, esta cabecera no se envía. Ver también + /// [`hx_trigger_name()`](Self::hx_trigger_name). + fn hx_trigger_id(&self) -> Option<&str>; + + /// Valor del atributo `name` del elemento que disparó la petición (`HX-Trigger-Name`). + /// + /// Especialmente útil en formularios, donde el elemento disparador puede tener `name` pero no + /// `id`. + fn hx_trigger_name(&self) -> Option<&str>; + + /// Texto introducido por el usuario en un diálogo `hx-prompt` (`HX-Prompt`). + /// + /// Sólo presente si el elemento tiene el atributo `hx-prompt` y el usuario no canceló el + /// diálogo. + fn hx_prompt(&self) -> Option<&str>; +} + +impl HtmxRequestExt for HttpRequest { + fn is_htmx(&self) -> bool { + header_equals(self.headers(), "hx-request", "true") + } + + fn is_boosted(&self) -> bool { + header_equals(self.headers(), "hx-boosted", "true") + } + + fn is_history_restore(&self) -> bool { + header_equals(self.headers(), "hx-history-restore-request", "true") + } + + fn hx_current_url(&self) -> Option<&str> { + header_str(self.headers(), "hx-current-url") + } + + fn hx_target(&self) -> Option<&str> { + header_str(self.headers(), "hx-target") + } + + fn hx_trigger_id(&self) -> Option<&str> { + header_str(self.headers(), "hx-trigger") + } + + fn hx_trigger_name(&self) -> Option<&str> { + header_str(self.headers(), "hx-trigger-name") + } + + fn hx_prompt(&self) -> Option<&str> { + header_str(self.headers(), "hx-prompt") + } +} + +fn header_equals(headers: &web::http::HeaderMap, name: &str, expected: &str) -> bool { + headers + .get(name) + .and_then(|v| v.to_str().ok()) + .map(|v| v == expected) + .unwrap_or(false) +} + +fn header_str<'a>(headers: &'a web::http::HeaderMap, name: &str) -> Option<&'a str> { + headers.get(name).and_then(|v| v.to_str().ok()) +} diff --git a/extensions/pagetop-htmx/src/response.rs b/extensions/pagetop-htmx/src/response.rs new file mode 100644 index 00000000..66550cf2 --- /dev/null +++ b/extensions/pagetop-htmx/src/response.rs @@ -0,0 +1,228 @@ +//! Implementación de [`HtmxResponse`] e [`IntoResponse`](pagetop::web::IntoResponse) para HTMX. + +use pagetop::prelude::*; + +// **< HtmxResponse >******************************************************************************* + +/// Generador de respuestas HTML parciales con cabeceras HTMX. +/// +/// En una aplicación HTMX, los *handlers* del servidor devuelven con frecuencia fragmentos HTML +/// parciales acompañados de cabeceras especiales que instruyen al cliente sobre qué hacer con la +/// respuesta: actualizar la URL del historial, disparar eventos JavaScript, redirigir, etc. +/// +/// Implementa [`IntoResponse`](pagetop::web::IntoResponse), por lo que puede devolverse +/// directamente desde cualquier *handler*. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_htmx::{HtmxResponse, hx}; +/// +/// async fn add_item(request: HttpRequest) -> impl IntoResponse { +/// let new_item = html! { li #item-42 { "New item" } }; +/// +/// HtmxResponse::new(new_item) +/// .retarget("#list") +/// .reswap(hx::swap::BEFORE_END) +/// .push_url("/items") +/// .trigger("itemAdded") +/// } +/// ``` +/// +/// # Respuestas de sólo cabeceras +/// +/// Cuando la respuesta no lleva cuerpo HTML (por ejemplo, una redirección o un refresco), usa +/// [`HtmxResponse::empty()`](Self::empty): +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_htmx::HtmxResponse; +/// +/// async fn delete_item() -> impl IntoResponse { +/// HtmxResponse::empty().redirect("/items") +/// } +/// ``` +/// +/// # Construcción +/// +/// - [`HtmxResponse::new(markup)`](Self::new), con el fragmento HTML. +/// - [`HtmxResponse::empty()`](Self::empty), sin cuerpo, sólo cabeceras. +/// +/// # Cabeceras disponibles +/// +/// Los nombres de cabecera como constantes están en [`crate::hx::response`]. +/// +/// # Múltiples eventos en `trigger` +/// +/// Para disparar varios eventos en una sola llamada, pasa una cadena con comas o un objeto JSON: +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// use pagetop_htmx::HtmxResponse; +/// +/// // Dos eventos sin datos: +/// HtmxResponse::empty().trigger("itemAdded, listUpdated"); +/// +/// // Evento con datos en JSON: +/// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42}}"#); +/// ``` +pub struct HtmxResponse { + markup: Markup, + headers: web::http::HeaderMap, +} + +impl HtmxResponse { + /// Crea una respuesta con el fragmento HTML indicado. + pub fn new(markup: Markup) -> Self { + Self { + markup, + headers: web::http::HeaderMap::new(), + } + } + + /// Crea una respuesta sin cuerpo HTML, útil para respuestas de sólo cabeceras. + pub fn empty() -> Self { + Self::new(html! {}) + } + + // **< HtmxResponse BUILDER >******************************************************************* + + /// Hace que HTMX realice una navegación AJAX a la URL indicada sin recargar la página. + /// + /// A diferencia de [`redirect()`](Self::redirect), la navegación usa HTMX y actualiza sólo el + /// objetivo definido por el destino. Acepta una URL o un objeto JSON con claves `path`, + /// `target`, `swap`, `select` y `values` para personalizar la navegación: + /// + /// ```rust,no_run + /// use pagetop::prelude::*; + /// use pagetop_htmx::HtmxResponse; + /// + /// // Navegación simple: + /// HtmxResponse::empty().location("/items"); + /// + /// // Navegación con destino personalizado: + /// HtmxResponse::empty() + /// .location(r##"{"path": "/items", "target": "#content"}"##); + /// ``` + pub fn location(self, url: impl Into) -> Self { + self.set_header(b"hx-location", url) + } + + /// Empuja la URL indicada al historial del navegador. + /// + /// El usuario podrá navegar hacia atrás hasta esa URL. Usar `"false"` para desactivar el empuje + /// aunque esté habilitado por el atributo `hx-push-url` del elemento. + pub fn push_url(self, url: impl Into) -> Self { + self.set_header(b"hx-push-url", url) + } + + /// Reemplaza la URL actual en el historial sin añadir una nueva entrada. + /// + /// Usar `"false"` para desactivar el reemplazo. + pub fn replace_url(self, url: impl Into) -> Self { + self.set_header(b"hx-replace-url", url) + } + + /// Provoca una redirección completa del navegador a la URL indicada. + /// + /// A diferencia de [`location()`](Self::location), esta redirección recarga la página por + /// completo, como un `window.location.href = url` en JavaScript. + pub fn redirect(self, url: impl Into) -> Self { + self.set_header(b"hx-redirect", url) + } + + /// Provoca una recarga completa de la página actual. + /// + /// Equivale a `window.location.reload()` en JavaScript. + pub fn refresh(self) -> Self { + self.set_header(b"hx-refresh", "true") + } + + /// Anula el `hx-target` del elemento y redirige la respuesta al selector CSS indicado. + /// + /// Útil cuando el servidor necesita actualizar un elemento distinto al que realizó la petición, + /// sin modificar el HTML del cliente. + pub fn retarget(self, selector: impl Into) -> Self { + self.set_header(b"hx-retarget", selector) + } + + /// Anula el `hx-swap` del elemento e impone la estrategia de sustitución indicada. + /// + /// Acepta los mismos valores que el atributo `hx-swap`, incluidos modificadores (`swap:200ms`, + /// `scroll:top`, ...). Los valores tipados están en [`crate::hx::swap`]. + pub fn reswap(self, strategy: impl Into) -> Self { + self.set_header(b"hx-reswap", strategy) + } + + /// Anula el `hx-select` del elemento y selecciona el fragmento CSS indicado de la respuesta + /// para insertarlo en el objetivo. + pub fn reselect(self, selector: impl Into) -> Self { + self.set_header(b"hx-reselect", selector) + } + + /// Dispara uno o varios eventos JavaScript en el cliente al completar la respuesta. + /// + /// Los eventos se disparan inmediatamente tras procesar la respuesta. Para disparar eventos con + /// datos o después de otras fases del ciclo HTMX, ver + /// [`trigger_after_settle()`](Self::trigger_after_settle) y + /// [`trigger_after_swap()`](Self::trigger_after_swap). + /// + /// ```rust,no_run + /// use pagetop::prelude::*; + /// use pagetop_htmx::HtmxResponse; + /// + /// // Evento simple: + /// HtmxResponse::empty().trigger("itemAdded"); + /// + /// // Múltiples eventos sin datos: + /// HtmxResponse::empty().trigger("itemAdded, listUpdated"); + /// + /// // Evento con datos en JSON: + /// HtmxResponse::empty().trigger(r#"{"itemAdded": {"id": 42, "name": "Example"}}"#); + /// ``` + pub fn trigger(self, event: impl Into) -> Self { + self.set_header(b"hx-trigger", event) + } + + /// Dispara eventos JavaScript después de que HTMX haya aplicado la respuesta al DOM y haya + /// completado la fase de *settle* (animaciones CSS). + /// + /// Acepta los mismos formatos que [`trigger()`](Self::trigger). + pub fn trigger_after_settle(self, event: impl Into) -> Self { + self.set_header(b"hx-trigger-after-settle", event) + } + + /// Dispara eventos JavaScript después de que HTMX haya aplicado la respuesta al DOM, pero antes + /// de la fase de *settle*. + /// + /// Acepta los mismos formatos que [`trigger()`](Self::trigger). + pub fn trigger_after_swap(self, event: impl Into) -> Self { + self.set_header(b"hx-trigger-after-swap", event) + } + + // Inserta o reemplaza una cabecera. Los nombres deben ser bytes ASCII en minúsculas. + fn set_header(mut self, name: &[u8], value: impl Into) -> Self { + let value = value.into(); + if let (Ok(n), Ok(v)) = ( + web::http::HeaderName::from_bytes(name), + web::http::HeaderValue::from_str(&value), + ) { + self.headers.insert(n, v); + } else { + trace::warn!(value = %value, "HtmxResponse: invalid header value, header discarded"); + } + self + } +} + +impl web::IntoResponse for HtmxResponse { + fn into_response(self) -> Response { + let mut headers = self.headers; + headers.insert( + web::http::header::CONTENT_TYPE, + web::http::HeaderValue::from_static("text/html; charset=utf-8"), + ); + (headers, self.markup.into_string()).into_response() + } +} diff --git a/extensions/pagetop-seaorm/README.md b/extensions/pagetop-seaorm/README.md index 23a4ce40..b9b7e1a4 100644 --- a/extensions/pagetop-seaorm/README.md +++ b/extensions/pagetop-seaorm/README.md @@ -11,14 +11,13 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## ⚡️ Guía rápida +## Guía rápida **Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites: @@ -59,7 +58,7 @@ impl Extension for MyApp { } fn initialize(&self) { - install_migrations!(m20240101_000001_create_users_table); + install_migrations!(m20240101_000001_create_users); } } @@ -146,8 +145,7 @@ async fn example() -> Result<(), DbErr> { } ``` - -## 📚 Créditos +## Créditos Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/SeaQL) como: @@ -179,15 +177,13 @@ extensión. Los ficheros adaptados del original son: | `schema.rs` | Integra con ajustes, adaptado de [loco](https://github.com/loco-rs/loco) | | `seaql_migrations.rs` | Integración completa | - -## 🚧 Advertencia +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/extensions/pagetop-seaorm/src/db.rs b/extensions/pagetop-seaorm/src/db.rs index a2b5dd4d..3076d834 100644 --- a/extensions/pagetop-seaorm/src/db.rs +++ b/extensions/pagetop-seaorm/src/db.rs @@ -3,7 +3,7 @@ //! Agrupa los *traits*, macros y tipos del sistema de entidades de SeaORM, junto con las funciones //! [`dbconn`], [`execute`], [`fetch_all`] y [`fetch_one`], en una sola importación: //! -//! ```rust +//! ```rust,no_run //! use pagetop_seaorm::db::*; //! ``` //! @@ -34,7 +34,7 @@ //! //! ## Definir una entidad //! -//! ```rust +//! ```rust,no_run //! use pagetop_seaorm::db::*; //! //! #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] @@ -111,7 +111,7 @@ //! El módulo [`api`] re-exporta el crate `sea_orm` íntegro bajo ese alias. Úsalo cuando necesites //! un tipo o función que no esté expuesto directamente en `db::*`: //! -//! ```rust +//! ```rust,no_run //! use pagetop_seaorm::db::api; //! //! // Tipos o utilidades no incluidos en db::*: @@ -123,7 +123,7 @@ //! El módulo [`query`] re-exporta `sea_query` para construir las sentencias SQL que se pasan a //! [`fetch_all`] y [`fetch_one`]. Es el compañero natural de esas funciones dentro del módulo `db`: //! -//! ```rust +//! ```rust,no_run //! use pagetop_seaorm::db::*; //! use pagetop_seaorm::db::query::*; //! @@ -193,7 +193,7 @@ pub fn dbconn() -> &'static DatabaseConnection { /// > **Advertencia:** nunca interpoles valores externos en la cadena SQL directamente. Para /// > sentencias con parámetros de usuario usa el sistema de entidades. /// -/// ```rust +/// ```rust,no_run /// use pagetop_seaorm::db::*; /// /// async fn example() -> Result<(), DbErr> { @@ -224,7 +224,7 @@ pub async fn execute(stmt: impl Into) -> Result { /// Los valores se integran como literales escapados, no como parámetros de base de datos. Para /// datos procedentes del usuario, el sistema de entidades es más robusto. /// -/// ```rust +/// ```rust,no_run /// use pagetop_seaorm::db::*; /// use pagetop_seaorm::db::query::*; /// @@ -274,7 +274,7 @@ pub async fn fetch_all( /// Los valores se integran como literales escapados, no como parámetros de base de datos. Para /// datos procedentes del usuario, el sistema de entidades es más robusto. /// -/// ```rust +/// ```rust,no_run /// use pagetop_seaorm::db::*; /// use pagetop_seaorm::db::query::*; /// diff --git a/extensions/pagetop-seaorm/src/lib.rs b/extensions/pagetop-seaorm/src/lib.rs index ef056f64..0d47dc1a 100644 --- a/extensions/pagetop-seaorm/src/lib.rs +++ b/extensions/pagetop-seaorm/src/lib.rs @@ -12,14 +12,13 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## ⚡️ Guía rápida +## Guía rápida **Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites: @@ -149,7 +148,7 @@ async fn example() -> Result<(), DbErr> { */ #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] use pagetop::prelude::*; diff --git a/extensions/pagetop-seaorm/src/migration.rs b/extensions/pagetop-seaorm/src/migration.rs index f3544f7f..2e267e8d 100644 --- a/extensions/pagetop-seaorm/src/migration.rs +++ b/extensions/pagetop-seaorm/src/migration.rs @@ -6,7 +6,7 @@ //! //! Con una sola importación tienes todo lo necesario: //! -//! ```rust +//! ```rust,no_run //! use pagetop_seaorm::migration::*; //! ``` //! diff --git a/helpers/pagetop-build/Cargo.toml b/helpers/pagetop-build/Cargo.toml index a06bc9ca..0e2dd844 100644 --- a/helpers/pagetop-build/Cargo.toml +++ b/helpers/pagetop-build/Cargo.toml @@ -3,8 +3,7 @@ name = "pagetop-build" version = "0.3.2" description = """ - Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el - binario de un proyecto PageTop. + Genera o prepara archivos estáticos para servirlos o incluirlos en un proyecto PageTop. """ categories = ["development-tools::build-utils"] keywords = ["pagetop", "build", "assets", "resources", "static"] @@ -17,4 +16,5 @@ authors.workspace = true [dependencies] grass.workspace = true +minify-js.workspace = true pagetop-statics.workspace = true diff --git a/helpers/pagetop-build/README.md b/helpers/pagetop-build/README.md index bb7d3bfa..e33bf8f0 100644 --- a/helpers/pagetop-build/README.md +++ b/helpers/pagetop-build/README.md @@ -2,7 +2,7 @@

PageTop Build

-

Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el binario de un proyecto PageTop.

+

Genera o prepara archivos estáticos para servirlos o incluirlos en un proyecto PageTop.

[![Doc API](https://img.shields.io/docsrs/pagetop-build?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-build) [![Crates.io](https://img.shields.io/crates/v/pagetop-build.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-build) @@ -11,91 +11,85 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. +## Guía rápida -## ⚡️ Guía rápida +La convención recomendada para extensiones, temas o aplicaciones basadas en **PageTop** es separar +los archivos fuente de los generados siguiendo el patrón `assets/` -> `static/`: -Añadir en el archivo `Cargo.toml` del proyecto: +- **`assets/`** - archivos versionados en el repositorio, por ejemplo archivos SCSS, JavaScript de + terceros, fuentes, etc. Todo lo que hay aquí se sube al repositorio y será la fuente para generar + el directorio final `static/`. +- **`static/`** - archivos generados en tiempo de compilación a partir de `assets/`. Se añade a + `.gitignore` y nunca se sube al repositorio. +- **`build.rs`** - orquesta la transformación: genera `static/` desde `assets/` para servirlos o + incluirlos en el proyecto. -```toml -[build-dependencies] -pagetop-build = { ... } -``` +Durante el desarrollo, `static/` existe en disco y los archivos se sirven desde ahí. En producción, +el directorio no existe y los recursos salen del binario. La macro +[`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) +gestiona esta dualidad de forma transparente. -Y crear un archivo `build.rs` a la altura de `Cargo.toml` para indicar cómo se van a incluir los -archivos estáticos o cómo se van a compilar los archivos SCSS para el proyecto. Casos de uso: +### Funciones de transformación -### Incluir archivos estáticos desde un directorio +Estas funciones se usan en el `build.rs` de cada proyecto para generar `static/` a partir de +`assets/`. Todas crean el directorio padre del destino si no existe y devuelven `io::Result<()>` +para poder propagarse con `?` en caso de error. -Hay que preparar una carpeta en el proyecto con todos los archivos que se quieren incluir, por -ejemplo `static`, y añadir el siguiente código en `build.rs` para crear el conjunto de recursos: +- `compile_scss()` - compila un archivo SCSS a CSS minificado. +- `copy_dir()` - copia recursivamente un directorio completo. Útil para copiar todos los archivos de + `assets/` o de un subdirectorio a `static/` sin transformación. +- `copy_file()` - copia un archivo al destino. +- `copy_file_replacing()` - copia un archivo aplicando una lista de sustituciones de texto en su + contenido; útil para actualizar referencias internas (p.ej. `sourceMappingURL`) al renombrar + archivos. +- `minify_js()` - minifica un archivo JavaScript. + +### Incluir los archivos estáticos en el proyecto + +Una vez generado `static/`, usaremos `StaticFilesBundle` para incluir su contenido en el binario. Se +pueden crear tantos paquetes de recursos como sea necesario, siempre que tengan nombres distintos: ```rust,no_run use pagetop_build::StaticFilesBundle; fn main() -> std::io::Result<()> { - StaticFilesBundle::from_dir("./static", None) - .with_name("guides") + StaticFilesBundle::from_dir("./static/css", None) + .with_name("app_css") + .build()?; + StaticFilesBundle::from_dir("./static/fonts", None) + .with_name("app_fonts") .build() } ``` -Si es necesario, se puede añadir un filtro para seleccionar archivos específicos de la carpeta, por -ejemplo: +Si es necesario excluir algunos archivos del paquete de recursos (p. ej. los archivos `.map` que no +son necesarios en producción), se puede pasar una función de filtro: ```rust,no_run use pagetop_build::StaticFilesBundle; use std::path::Path; fn main() -> std::io::Result<()> { - fn only_pdf_files(path: &Path) -> bool { - // Selecciona únicamente los archivos con extensión `.pdf`. - path.extension().map_or(false, |ext| ext == "pdf") - } - - StaticFilesBundle::from_dir("./static", Some(only_pdf_files)) - .with_name("guides") + StaticFilesBundle::from_dir("./static/js", Some(only_js)) + .with_name("app_js") .build() } + +fn only_js(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "js") +} ``` -### Compilar archivos SCSS a CSS - -Se puede compilar un archivo SCSS, que podría importar otros a su vez, para preparar un recurso con -el archivo CSS minificado obtenido. Por ejemplo: - -```rust,no_run -use pagetop_build::StaticFilesBundle; - -fn main() -> std::io::Result<()> { - StaticFilesBundle::from_scss("./styles/main.scss", "styles.min.css") - .with_name("main_styles") - .build() -} -``` - -Este código compila el archivo `main.scss` de la carpeta `static` del proyecto, y prepara un recurso -llamado `main_styles` que contiene el archivo `styles.min.css` obtenido. - - -## 📦 Archivos generados - -Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar -[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts) -donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para -`with_name("guides")` se genera un archivo llamado `guides.rs`. - -No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se -usen nombres diferentes. - -Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) -para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: +Cada paquete de recursos genera un archivo `.rs` en +[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts). +No es necesario acceder a él directamente: el nombre asignado con `.with_name()` se usa como +identificador en `serve_static_files!` para configurar la ruta del servicio: ```rust,ignore use pagetop::prelude::*; @@ -103,22 +97,54 @@ use pagetop::prelude::*; pub struct MyExtension; impl Extension for MyExtension { - // Servicio web que publica los recursos de `guides` en `/ruta/a/guides`. - fn configure_service(&self, scfg: &mut service::web::ServiceConfig) { - serve_static_files!(scfg, guides => "/ruta/a/guides"); + fn configure_router(&self, mut router: Router) -> Router { + serve_static_files!(router, ["./static/css", app_css] => "/public/css"); + router } } ``` +## Ejemplo completo -## 🚧 Advertencia +```rust,no_run +use pagetop_build::StaticFilesBundle; +use pagetop_build::{compile_scss, copy_file, copy_file_replacing, minify_js}; +use std::path::Path; + +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"); + + // Genera `static/` a partir de `assets/`. + compile_scss("assets/main.scss", "static/css/main.min.css")?; + copy_file("assets/fonts/icon.woff2", "static/fonts/icon.woff2")?; + copy_file_replacing( + "assets/lib.min.js", + "static/js/app.min.js", + &[("lib.min.js.map", "app.min.js.map")], + )?; + minify_js("assets/shell.js", "static/js/shell.min.js")?; + + // Prepara los paquetes de recursos para incluir en el proyecto. + StaticFilesBundle::from_dir("./static/css", None).with_name("app_css").build()?; + StaticFilesBundle::from_dir("./static/js", Some(only_js)).with_name("app_js").build()?; + StaticFilesBundle::from_dir("./static/fonts", None).with_name("app_fonts").build() +} + +// Los `.map` no se incluyen, se servirán desde disco durante el desarrollo. +fn only_js(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "js") +} +``` + +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/helpers/pagetop-build/src/lib.rs b/helpers/pagetop-build/src/lib.rs index eacb6180..5a9d24bb 100644 --- a/helpers/pagetop-build/src/lib.rs +++ b/helpers/pagetop-build/src/lib.rs @@ -3,7 +3,7 @@

PageTop Build

-

Prepara un conjunto de archivos estáticos o archivos SCSS compilados para ser incluidos en el binario de un proyecto PageTop.

+

Genera o prepara archivos estáticos para servirlos o incluirlos en un proyecto PageTop.

[![Doc API](https://img.shields.io/docsrs/pagetop-build?label=Doc%20API&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop-build) [![Crates.io](https://img.shields.io/crates/v/pagetop-build.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop-build) @@ -18,85 +18,79 @@ clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. +## Guía rápida -# ⚡️ Guía rápida +La convención recomendada para extensiones, temas o aplicaciones basadas en **PageTop** es separar +los archivos fuente de los generados siguiendo el patrón `assets/` -> `static/`: -Añadir en el archivo `Cargo.toml` del proyecto: +- **`assets/`** - archivos versionados en el repositorio, por ejemplo archivos SCSS, JavaScript de + terceros, fuentes, etc. Todo lo que hay aquí se sube al repositorio y será la fuente para generar + el directorio final `static/`. +- **`static/`** - archivos generados en tiempo de compilación a partir de `assets/`. Se añade a + `.gitignore` y nunca se sube al repositorio. +- **`build.rs`** - orquesta la transformación: genera `static/` desde `assets/` para servirlos o + incluirlos en el proyecto. -```toml -[build-dependencies] -pagetop-build = { ... } -``` +Durante el desarrollo, `static/` existe en disco y los archivos se sirven desde ahí. En producción, +el directorio no existe y los recursos salen del binario. La macro +[`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) +gestiona esta dualidad de forma transparente. -Y crear un archivo `build.rs` a la altura de `Cargo.toml` para indicar cómo se van a incluir los -archivos estáticos o cómo se van a compilar los archivos SCSS para el proyecto. Casos de uso: +### Funciones de transformación -## Incluir archivos estáticos desde un directorio +Estas funciones se usan en el `build.rs` de cada proyecto para generar `static/` a partir de +`assets/`. Todas crean el directorio padre del destino si no existe y devuelven `io::Result<()>` +para poder propagarse con `?` en caso de error. -Hay que preparar una carpeta en el proyecto con todos los archivos que se quieren incluir, por -ejemplo `static`, y añadir el siguiente código en `build.rs` para crear el conjunto de recursos: +- `compile_scss()` - compila un archivo SCSS a CSS minificado. +- `copy_dir()` - copia recursivamente un directorio completo. Útil para copiar todos los archivos de + `assets/` o de un subdirectorio a `static/` sin transformación. +- `copy_file()` - copia un archivo al destino. +- `copy_file_replacing()` - copia un archivo aplicando una lista de sustituciones de texto en su + contenido; útil para actualizar referencias internas (p.ej. `sourceMappingURL`) al renombrar + archivos. +- `minify_js()` - minifica un archivo JavaScript. + +### Incluir los archivos estáticos en el proyecto + +Una vez generado `static/`, usaremos `StaticFilesBundle` para incluir su contenido en el binario. Se +pueden crear tantos paquetes de recursos como sea necesario, siempre que tengan nombres distintos: ```rust,no_run use pagetop_build::StaticFilesBundle; fn main() -> std::io::Result<()> { - StaticFilesBundle::from_dir("./static", None) - .with_name("guides") + StaticFilesBundle::from_dir("./static/css", None) + .with_name("app_css") + .build()?; + StaticFilesBundle::from_dir("./static/fonts", None) + .with_name("app_fonts") .build() } ``` -Si es necesario, se puede añadir un filtro para seleccionar archivos específicos de la carpeta, por -ejemplo: +Si es necesario excluir algunos archivos del paquete de recursos (p. ej. los archivos `.map` que no +son necesarios en producción), se puede pasar una función de filtro: ```rust,no_run use pagetop_build::StaticFilesBundle; use std::path::Path; fn main() -> std::io::Result<()> { - fn only_pdf_files(path: &Path) -> bool { - // Selecciona únicamente los archivos con extensión `.pdf`. - path.extension().map_or(false, |ext| ext == "pdf") - } - - StaticFilesBundle::from_dir("./static", Some(only_pdf_files)) - .with_name("guides") + StaticFilesBundle::from_dir("./static/js", Some(only_js)) + .with_name("app_js") .build() } + +fn only_js(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "js") +} ``` -## Compilar archivos SCSS a CSS - -Se puede compilar un archivo SCSS, que podría importar otros a su vez, para preparar un recurso con -el archivo CSS minificado obtenido. Por ejemplo: - -```rust,no_run -use pagetop_build::StaticFilesBundle; - -fn main() -> std::io::Result<()> { - StaticFilesBundle::from_scss("./styles/main.scss", "styles.min.css") - .with_name("main_styles") - .build() -} -``` - -Este código compila el archivo `main.scss` de la carpeta `static` del proyecto, y prepara un recurso -llamado `main_styles` que contiene el archivo `styles.min.css` obtenido. - - -# 📦 Archivos generados - -Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar -[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts) -donde se incluye el código necesario para compilar el proyecto. Por ejemplo, para -`with_name("guides")` se genera un archivo llamado `guides.rs`. - -No hay ningún problema en generar más de un conjunto de recursos para cada proyecto siempre que se -usen nombres diferentes. - -Normalmente no habrá que acceder a estos módulos; sólo declarar el nombre del conjunto de recursos -en [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) -para configurar un servicio web que sirva los archivos desde la ruta indicada. Por ejemplo: +Cada paquete de recursos genera un archivo `.rs` en +[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts). +No es necesario acceder a él directamente: el nombre asignado con `.with_name()` se usa como +identificador en `serve_static_files!` para configurar la ruta del servicio: ```rust,ignore use pagetop::prelude::*; @@ -104,39 +98,77 @@ use pagetop::prelude::*; pub struct MyExtension; impl Extension for MyExtension { - /// Registra los recursos de `guides` en el router bajo `/ruta/a/guides`. fn configure_router(&self, mut router: Router) -> Router { - serve_static_files!(router, [guides] => "/ruta/a/guides"); + serve_static_files!(router, ["./static/css", app_css] => "/public/css"); router } } ``` + +## Ejemplo completo + +```rust,no_run +use pagetop_build::StaticFilesBundle; +use pagetop_build::{compile_scss, copy_file, copy_file_replacing, minify_js}; +use std::path::Path; + +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"); + + // Genera `static/` a partir de `assets/`. + compile_scss("assets/main.scss", "static/css/main.min.css")?; + copy_file("assets/fonts/icon.woff2", "static/fonts/icon.woff2")?; + copy_file_replacing( + "assets/lib.min.js", + "static/js/app.min.js", + &[("lib.min.js.map", "app.min.js.map")], + )?; + minify_js("assets/shell.js", "static/js/shell.min.js")?; + + // Prepara los paquetes de recursos para incluir en el proyecto. + StaticFilesBundle::from_dir("./static/css", None).with_name("app_css").build()?; + StaticFilesBundle::from_dir("./static/js", Some(only_js)).with_name("app_js").build()?; + StaticFilesBundle::from_dir("./static/fonts", None).with_name("app_fonts").build() +} + +// Los `.map` no se incluyen, se servirán desde disco durante el desarrollo. +fn only_js(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "js") +} +``` */ #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] use grass::{Options, OutputStyle, from_path}; -use pagetop_statics::{ResourceDir, resource_dir}; +use minify_js::{Session, TopLevelMode, minify}; +use pagetop_statics::resource_dir; -use std::fs::{File, create_dir_all, remove_dir_all}; -use std::io::Write; -use std::path::Path; +use std::fs::{File, copy as fs_copy, create_dir_all, read_dir}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; -/// Prepara un conjunto de recursos para ser incluidos en el binario del proyecto. +// **< StaticFilesBundle >************************************************************************** + +/// Prepara un paquete de recursos para incluir en el binario del proyecto. pub struct StaticFilesBundle { - resource_dir: ResourceDir, + dir: PathBuf, + filter: Option bool>, + name: Option, } impl StaticFilesBundle { - /// Prepara el conjunto de recursos con los archivos de un directorio. Opcionalmente se puede - /// aplicar un filtro para seleccionar un subconjunto de los archivos. + /// Crea el paquete de recursos con los archivos del directorio indicado. /// /// # Argumentos /// - /// * `dir` - Directorio que contiene los archivos. - /// * `filter` - Una función opcional para aceptar o no un archivo según su ruta. + /// * `dir` - Ruta al directorio con los archivos a incluir, normalmente `static/` o un + /// directorio dentro de este. + /// * `filter` - Función opcional para seleccionar qué archivos incluir en el paquete. /// /// # Ejemplo /// @@ -145,124 +177,227 @@ impl StaticFilesBundle { /// use std::path::Path; /// /// fn main() -> std::io::Result<()> { - /// fn only_images(path: &Path) -> bool { - /// matches!( - /// path.extension().and_then(|ext| ext.to_str()), - /// Some("jpg" | "png" | "gif") - /// ) - /// } - /// /// StaticFilesBundle::from_dir("./static", Some(only_images)) /// .with_name("images") /// .build() /// } + /// + /// fn only_images(path: &Path) -> bool { + /// matches!( + /// path.extension().and_then(|ext| ext.to_str()), + /// Some("jpg" | "png" | "gif") + /// ) + /// } /// ``` pub fn from_dir

(dir: P, filter: Option bool>) -> Self where P: AsRef, { - let dir_path = dir.as_ref(); - let dir_str = dir_path.to_str().unwrap_or_else(|| { - panic!( - "Resource directory path is not valid UTF-8: {}", - dir_path.display() - ); - }); - - let mut resource_dir = resource_dir(dir_str); - - // Aplica el filtro si está definido. - if let Some(f) = filter { - resource_dir.with_filter(f); + Self { + dir: dir.as_ref().to_path_buf(), + filter, + name: None, } - - // Identifica el directorio temporal de recursos. - StaticFilesBundle { resource_dir } } - /// Prepara un recurso CSS minimizado a partir de la compilación de un archivo SCSS (que puede a - /// su vez importar otros archivos SCSS). + /// Asigna un nombre al paquete de recursos. /// - /// # Argumentos + /// El nombre debe ser un identificador Rust válido que se convertirá en nombre del módulo y de + /// la función del archivo `.rs` generado en `OUT_DIR`. Si no se llama a este método, el nombre + /// por defecto será `"bundle"`. /// - /// * `path` - Archivo SCSS a compilar. - /// * `target_name` - Nombre para el archivo CSS. + /// Este nombre es el que hay que declarar en + /// [`serve_static_files!`](https://docs.rs/pagetop/latest/pagetop/macro.serve_static_files.html) + /// para configurar la ruta del servicio: /// - /// # Ejemplo - /// - /// ```rust,no_run - /// use pagetop_build::StaticFilesBundle; - /// - /// fn main() -> std::io::Result<()> { - /// StaticFilesBundle::from_scss("./bootstrap/scss/main.scss", "bootstrap.min.css") - /// .with_name("bootstrap_css") - /// .build() - /// } + /// ```rust,ignore + /// serve_static_files!(router, ["./static/css", app_css] => "/public/css"); + /// // ^^^^^^^ + /// // debe coincidir con .with_name("app_css") /// ``` - pub fn from_scss

(path: P, target_name: &str) -> Self - where - P: AsRef, - { - // Crea un directorio temporal único para el archivo CSS (basado en su nombre, para que - // varias llamadas a from_scss en el mismo build.rs no se pisen). - let out_dir = std::env::var("OUT_DIR").unwrap(); - let safe_name = target_name.replace(['.', '-'], "_"); - let temp_dir = Path::new(&out_dir).join(format!("from_scss_{safe_name}")); - - // Limpia el directorio temporal de ejecuciones previas, si existe. - if temp_dir.exists() { - remove_dir_all(&temp_dir).unwrap_or_else(|e| { - panic!( - "Failed to clean temporary directory `{}`: {e}", - temp_dir.display() - ); - }); - } - create_dir_all(&temp_dir).unwrap_or_else(|e| { - panic!( - "Failed to create temporary directory `{}`: {e}", - temp_dir.display() - ); - }); - - // Compila SCSS a CSS. - let css_content = from_path( - path.as_ref(), - &Options::default().style(OutputStyle::Compressed), - ) - .unwrap_or_else(|e| { - panic!( - "Failed to compile SCSS file `{}`: {e}", - path.as_ref().display(), - ) - }); - - // Guarda el archivo CSS compilado en el directorio temporal. - let css_path = temp_dir.join(target_name); - File::create(&css_path) - .unwrap_or_else(|_| panic!("Failed to create CSS file `{}`", css_path.display())) - .write_all(css_content.as_bytes()) - .unwrap_or_else(|_| panic!("Failed to write CSS content to `{}`", css_path.display())); - - // Identifica el directorio temporal de recursos. - StaticFilesBundle { - resource_dir: resource_dir(temp_dir.to_str().unwrap()), - } - } - - /// Asigna un nombre al conjunto de recursos. pub fn with_name(mut self, name: impl AsRef) -> Self { - let name = name.as_ref(); - let out_dir = std::env::var("OUT_DIR").unwrap(); - let filename = Path::new(&out_dir).join(format!("{name}.rs")); - self.resource_dir.with_generated_filename(filename); - self.resource_dir.with_module_name(format!("bundle_{name}")); - self.resource_dir.with_generated_fn(name); + self.name = Some(name.as_ref().to_string()); self } - /// Contruye finalmente el conjunto de recursos para incluir en el binario de la aplicación. + /// Genera el archivo `.rs` en `OUT_DIR` para incluir los recursos del directorio en el binario. pub fn build(self) -> std::io::Result<()> { - self.resource_dir.build() + let out_dir = std::env::var("OUT_DIR").unwrap(); + let name = self.name.as_deref().unwrap_or("bundle"); + + let mut rd = resource_dir(&self.dir); + if let Some(f) = self.filter { + rd.with_filter(f); + } + + let generated_filename = PathBuf::from(&out_dir).join(format!("{name}.rs")); + rd.with_generated_filename(generated_filename); + rd.with_module_name(format!("bundle_{name}")); + rd.with_generated_fn(name); + rd.build() } } + +// **< compile_scss / copy_dir / copy_file / copy_file_replacing / minify_js >********************** + +/// Compila un archivo SCSS a CSS minificado y lo escribe en la ruta de destino. +/// +/// Crea el directorio padre del destino si no existe. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// fn main() -> std::io::Result<()> { +/// pagetop_build::compile_scss("assets/main.scss", "static/css/main.min.css") +/// } +/// ``` +pub fn compile_scss(src: P, dst: Q) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let src = src.as_ref(); + let dst = dst.as_ref(); + + if let Some(parent) = dst.parent() { + create_dir_all(parent)?; + } + + let options = Options::default().style(OutputStyle::Compressed); + let css = from_path(src, &options) + .map_err(|e| io::Error::other(format!("failed to compile `{}`: {e}", src.display())))?; + File::create(dst)?.write_all(css.as_bytes()) +} + +/// Copia recursivamente el contenido de un directorio a otro destino. +/// +/// Crea el directorio destino y todos los subdirectorios necesarios. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// fn main() -> std::io::Result<()> { +/// pagetop_build::copy_dir("assets", "static") +/// } +/// ``` +pub fn copy_dir(src: P, dst: Q) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let src = src.as_ref(); + let dst = dst.as_ref(); + create_dir_all(dst)?; + for entry in read_dir(src)? { + let entry = entry?; + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if src_path.is_dir() { + copy_dir(&src_path, &dst_path)?; + } else { + fs_copy(&src_path, &dst_path)?; + } + } + Ok(()) +} + +/// Copia un archivo a su destino. +/// +/// Crea el directorio padre del destino si no existe. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// fn main() -> std::io::Result<()> { +/// pagetop_build::copy_file("assets/fonts/icon.woff2", "static/fonts/icon.woff2") +/// } +/// ``` +pub fn copy_file(src: P, dst: Q) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let src = src.as_ref(); + let dst = dst.as_ref(); + + if let Some(parent) = dst.parent() { + create_dir_all(parent)?; + } + + fs_copy(src, dst)?; + Ok(()) +} + +/// Copia un archivo a su destino con una lista de sustituciones de texto en su contenido. +/// +/// El archivo fuente se lee como texto UTF-8; no debe usarse con archivos binarios. Las +/// sustituciones de texto se aplican en orden y de forma encadenada: el resultado de cada +/// sustitución puede ser entrada de la siguiente. +/// +/// Crea el directorio padre del destino si no existe. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// fn main() -> std::io::Result<()> { +/// pagetop_build::copy_file_replacing( +/// "assets/adminlte.min.js", +/// "static/js/myapp.min.js", +/// &[("adminlte.min.js.map", "myapp.min.js.map")], +/// ) +/// } +/// ``` +pub fn copy_file_replacing(src: P, dst: Q, replacements: &[(&str, &str)]) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let src = src.as_ref(); + let dst = dst.as_ref(); + + if let Some(parent) = dst.parent() { + create_dir_all(parent)?; + } + + let content = std::fs::read_to_string(src)?; + let patched = replacements + .iter() + .fold(content, |acc, (old, new)| acc.replace(old, new)); + File::create(dst)?.write_all(patched.as_bytes()) +} + +/// Minifica un archivo JavaScript y lo escribe en la ruta de destino. +/// +/// El archivo se procesa en modo de ámbito global (`TopLevelMode::Global`), adecuado para scripts +/// sin `import`/`export`. Los archivos con sintaxis de módulo ES deben procesarse con +/// `TopLevelMode::Module`, que el *crate* subyacente (`minify-js`) también soporta pero esta +/// función no expone actualmente. +/// +/// Crea el directorio padre del destino si no existe. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// fn main() -> std::io::Result<()> { +/// pagetop_build::minify_js("assets/shell.js", "static/js/shell.min.js") +/// } +/// ``` +pub fn minify_js(src: P, dst: Q) -> io::Result<()> +where + P: AsRef, + Q: AsRef, +{ + let src = src.as_ref(); + let dst = dst.as_ref(); + + if let Some(parent) = dst.parent() { + create_dir_all(parent)?; + } + + let source = std::fs::read(src)?; + let session = Session::new(); + let mut output = Vec::new(); + minify(&session, TopLevelMode::Global, &source, &mut output) + .map_err(|e| io::Error::other(format!("failed to minify `{}`: {e:?}", src.display())))?; + File::create(dst)?.write_all(&output) +} diff --git a/helpers/pagetop-macros/README.md b/helpers/pagetop-macros/README.md index 9b0174a6..599f81bb 100644 --- a/helpers/pagetop-macros/README.md +++ b/helpers/pagetop-macros/README.md @@ -11,14 +11,13 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## 📚 Créditos +## Créditos Este *crate* incluye entre sus macros una adaptación de [maud-macros](https://crates.io/crates/maud_macros) @@ -29,15 +28,13 @@ Este *crate* incluye entre sus macros una adaptación de necesidad de referenciar `maud` o `smart_default` en las dependencias del archivo `Cargo.toml` de cada proyecto PageTop. - -## 🚧 Advertencia +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/helpers/pagetop-macros/src/lib.rs b/helpers/pagetop-macros/src/lib.rs index 63349aa0..4557196e 100644 --- a/helpers/pagetop-macros/src/lib.rs +++ b/helpers/pagetop-macros/src/lib.rs @@ -31,7 +31,7 @@ cada proyecto PageTop. */ #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] mod maud; @@ -126,7 +126,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// /// Si defines un método `with_` como este: /// -/// ```rust +/// ```rust,no_run /// # use pagetop_macros::builder_fn; /// # struct Example {value: Option}; /// # impl Example { @@ -140,7 +140,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream { /// /// la macro reescribirá el método `with_` y generará un nuevo método `alter_`: /// -/// ```rust +/// ```rust,no_run /// # struct Example {value: Option}; /// # impl Example { /// #[inline] diff --git a/helpers/pagetop-minimal/README.md b/helpers/pagetop-minimal/README.md index 1f8ec148..b7a17bc0 100644 --- a/helpers/pagetop-minimal/README.md +++ b/helpers/pagetop-minimal/README.md @@ -11,21 +11,19 @@ -## 🧭 Sobre PageTop +## Sobre PageTop [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y configurables, basadas en HTML, CSS y JavaScript. - -## 🗺️ Descripción general +## Descripción general Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la concatenación de cadenas y el uso rápido de colecciones clave-valor. - -## 📚 Créditos +## Créditos Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del *crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay). @@ -39,15 +37,13 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del * [pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular `paste!` de [David Tolnay](https://crates.io/users/dtolnay). - -## 🚧 Advertencia +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/helpers/pagetop-minimal/src/lib.rs b/helpers/pagetop-minimal/src/lib.rs index 3b8c9036..eab70c84 100644 --- a/helpers/pagetop-minimal/src/lib.rs +++ b/helpers/pagetop-minimal/src/lib.rs @@ -40,7 +40,7 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del * */ #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] #[doc(hidden)] diff --git a/helpers/pagetop-statics/README.md b/helpers/pagetop-statics/README.md index 3184f095..92541096 100644 --- a/helpers/pagetop-statics/README.md +++ b/helpers/pagetop-statics/README.md @@ -31,15 +31,13 @@ Para ello, adapta el código de [static-files](https://crates.io/crates/static_f se integra en PageTop para evitar que cada proyecto tenga que declarar `static-files` manualmente como dependencia en su `Cargo.toml`. - -## 🚧 Advertencia +## Advertencia **PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos hasta que se libere la versión **1.0.0**. - -## 📜 Licencia +## Licencia El código está disponible bajo una doble licencia: diff --git a/helpers/pagetop-statics/src/lib.rs b/helpers/pagetop-statics/src/lib.rs index d72176c6..426991d5 100644 --- a/helpers/pagetop-statics/src/lib.rs +++ b/helpers/pagetop-statics/src/lib.rs @@ -35,12 +35,14 @@ como dependencia en su `Cargo.toml`. #![doc(test(no_crate_inject))] #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] #![allow(clippy::needless_doctest_main)] /// Resource definition and single module based generation. pub mod resource; + +#[doc(inline)] pub use resource::Resource as StaticFile; mod resource_dir; diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 0778e9c8..08eefdb5 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct AfterRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for AfterRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl AfterRender { AfterRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl AfterRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index 051a3dd6..e91589a2 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct BeforeRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for BeforeRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl BeforeRender { BeforeRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl BeforeRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/action/component/transform_markup_component.rs b/src/base/action/component/transform_markup_component.rs index 3e3a81f5..bed2e192 100644 --- a/src/base/action/component/transform_markup_component.rs +++ b/src/base/action/component/transform_markup_component.rs @@ -6,7 +6,7 @@ use super::FnActionTransformMarkup; pub struct TransformMarkup { f: FnActionTransformMarkup, referer_type_id: Option, - referer_id: AttrId, + referer_id: Option, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for TransformMarkup { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.get() + self.referer_id.clone() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl TransformMarkup { TransformMarkup { f, referer_type_id: Some(UniqueId::of::()), - referer_id: AttrId::default(), + referer_id: None, weight: 0, } } @@ -42,7 +42,8 @@ impl TransformMarkup { /// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el /// componente `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - self.referer_id.alter_id(id); + let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); + self.referer_id = if id.is_empty() { None } else { Some(id) }; self } diff --git a/src/base/component.rs b/src/base/component.rs index 7ea596d3..5c9fee3d 100644 --- a/src/base/component.rs +++ b/src/base/component.rs @@ -1,11 +1,18 @@ //! Componentes nativos proporcionados por PageTop. -mod html; -pub use html::Html; - mod block; pub use block::Block; +mod button; +pub use button::{Button, ButtonAction}; + +pub mod form; +#[doc(inline)] +pub use form::Form; + +mod html; +pub use html::Html; + mod intro; pub use intro::{Intro, IntroOpening}; diff --git a/src/base/component/block.rs b/src/base/component/block.rs index 01f10d42..2e56c5b0 100644 --- a/src/base/component/block.rs +++ b/src/base/component/block.rs @@ -6,10 +6,8 @@ use crate::prelude::*; /// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*). #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Block { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS asociadas al bloque. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el título del bloque. title: L10n, /// Devuelve la lista de componentes hijo del bloque. @@ -22,11 +20,15 @@ impl Component for Block { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, "block"); + fn setup(&mut self, cx: &Context) { + // Asegura que el bloque tiene un identificador único. + self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); + + // Todos los bloques tienen la clase CSS `block` por defecto. + self.alter_prop(PropsOp::prepend_classes("block")); } fn prepare(&self, cx: &mut Context) -> Result { @@ -36,14 +38,12 @@ impl Component for Block { return Ok(html! {}); } - let id = cx.required_id::(self.id(), 1); - Ok(html! { - div id=(&id) class=[self.classes().get()] { + div (self.props()) { @if let Some(title) = self.title().lookup(cx) { - h2 class="block__title" { span { (title) } } + h2 class="block-title" { span { (title) } } } - div class="block__body" { (block_body) } + div class="block-body" { (block_body) } } }) } @@ -52,17 +52,17 @@ impl Component for Block { impl Block { // **< Block BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del bloque. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al bloque. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/src/base/component/button.rs b/src/base/component/button.rs new file mode 100644 index 00000000..8d2f0fa7 --- /dev/null +++ b/src/base/component/button.rs @@ -0,0 +1,207 @@ +use crate::prelude::*; + +use std::fmt; + +// **< ButtonAction >******************************************************************************* + +/// Comportamiento de un [`Button`] al activarse. +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum ButtonAction { + /// Envía un formulario al servidor. Es el **tipo por defecto**. + #[default] + Submit, + /// Restablece todos los campos de un formulario a sus valores iniciales. + Reset, + /// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse + /// mediante JavaScript. + Plain, +} + +impl fmt::Display for ButtonAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ButtonAction::Submit => "submit", + ButtonAction::Reset => "reset", + ButtonAction::Plain => "button", + }) + } +} + +// **< Button >************************************************************************************* + +/// Componente para crear un **botón**. +/// +/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`, +/// `reset` y botón genérico). +/// +/// El comportamiento del botón se establece al crearlo: +/// +/// - [`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. +/// +/// El botón puede usarse dentro o fuera de un formulario. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let save = Button::submit(L10n::n("Save")); +/// let cancel = Button::plain(L10n::n("Cancel")); +/// let clear = Button::reset(L10n::n("Clear")); +/// ``` +/// +/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del +/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar +/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// #[serde(default)] +/// action: Option, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Button { + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, + /// Devuelve el comportamiento del botón al activarse. + kind: ButtonAction, + /// Devuelve el nombre del botón. + name: AttrName, + /// Devuelve el valor del botón. + value: AttrValue, + /// Devuelve la etiqueta del botón. + label: Attr, + /// Devuelve si el botón recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si el botón está deshabilitado. + disabled: bool, +} + +impl Component for Button { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + self.alter_prop(PropsOp::prepend_classes("button")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + Ok(html! { + button + type=(self.kind()) + (self.props()) + name=[self.name().get()] + value=[self.value().get()] + autofocus[*self.autofocus()] + disabled[*self.disabled()] + { + @if let Some(label) = self.label().lookup(cx) { + (label) + } + } + }) + } +} + +impl Button { + /// Crea un botón de **envío** (`type="submit"`). + /// + /// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los + /// datos al servidor. + pub fn submit(label: L10n) -> Self { + Self { + kind: ButtonAction::Submit, + label: Attr::some(label), + ..Default::default() + } + } + + /// Crea un botón de **restablecimiento** (`type="reset"`). + /// + /// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales. + pub fn reset(label: L10n) -> Self { + Self { + kind: ButtonAction::Reset, + label: Attr::some(label), + ..Default::default() + } + } + + /// Crea un **botón genérico** (`type="button"`). + /// + /// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede + /// definirse mediante JavaScript. + pub fn plain(label: L10n) -> Self { + Self { + kind: ButtonAction::Plain, + label: Attr::some(label), + ..Default::default() + } + } + + // **< Button BUILDER >************************************************************************* + + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); + self + } + + /// Establece el nombre del botón (atributo `name`). + /// + /// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par + /// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + /// Establece el valor del botón (atributo `value`). + /// + /// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón + /// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario. + #[builder_fn] + pub fn with_value(mut self, value: impl AsRef) -> Self { + self.value.alter_str(value); + self + } + + /// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla). + #[builder_fn] + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece si el botón recibe el foco automáticamente al cargar la página. + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + /// Establece si el botón está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} diff --git a/src/base/component/form.rs b/src/base/component/form.rs new file mode 100644 index 00000000..a656bb6b --- /dev/null +++ b/src/base/component/form.rs @@ -0,0 +1,30 @@ +//! Componentes y tipos para crear formularios HTML ([`Form`]). + +mod props; +pub use props::{Autocomplete, AutofillField, CheckboxKind, Method}; + +mod component; +pub use component::Form; + +mod fieldset; +pub use fieldset::Fieldset; + +mod checkbox; +pub use checkbox::Checkbox; + +pub mod check; + +pub mod radio; + +pub mod select; + +pub mod input; + +mod textarea; +pub use textarea::Textarea; + +mod range; +pub use range::Range; + +mod hidden; +pub use hidden::Hidden; diff --git a/extensions/pagetop-bootsier/src/theme/form/check.rs b/src/base/component/form/check.rs similarity index 83% rename from extensions/pagetop-bootsier/src/theme/form/check.rs rename to src/base/component/form/check.rs index 434b7e6c..2bbc1091 100644 --- a/extensions/pagetop-bootsier/src/theme/form/check.rs +++ b/src/base/component/form/check.rs @@ -1,10 +1,10 @@ //! Definiciones para crear grupos de casillas de verificación (*check buttons*). -use pagetop::prelude::*; +use crate::prelude::*; // **< Item >*************************************************************************************** -/// Casilla de verificación individual de un [`form::check::Field`](Field). +/// Casilla de verificación individual de un [`Field`]. /// /// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable /// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto. @@ -15,9 +15,9 @@ use pagetop::prelude::*; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] @@ -65,24 +65,21 @@ impl Item { /// Componente para crear un **grupo de casillas de verificación**. /// -/// Renderiza un conjunto de casillas de verificación donde, a diferencia de un grupo de botones -/// [`form::radio::Field`](crate::theme::form::radio::Field), cada casilla puede marcarse de forma -/// independiente. -/// -/// Las casillas se añaden mediante [`with_item()`](Field::with_item) usando instancias de -/// [`form::check::Item`](Item). Si se activa el modo en línea con +/// Renderiza un conjunto de casillas de verificación donde cada casilla puede marcarse de forma +/// independiente. Las casillas se añaden con [`with_item()`](Field::with_item) usando instancias +/// de [`form::check::Item`]. Si se activa el modo en línea con /// [`with_inline()`](Field::with_inline), las casillas se disponen horizontalmente. /// /// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo -/// y el `name` del [`form::check::Item`](Item) con un guion bajo. Por ejemplo, para el grupo con +/// y el `name` del [`form::check::Item`] con un guion bajo. Por ejemplo, para el grupo con /// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y /// `name=interests_tech`. /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let interests = form::check::Field::new() /// .with_name("interests") /// .with_label(L10n::n("Areas of interest")) @@ -108,10 +105,8 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del grupo. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el nombre base compartido por todas las casillas del grupo. name: AttrName, /// Devuelve la etiqueta del grupo. @@ -132,21 +127,31 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, "form-field form-field-checkboxes"); - } - - fn prepare(&self, cx: &mut Context) -> Result { + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. let name = self .name() .get() .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + self.alter_name(&name); let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor del grupo de casillas. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + Ok(html! { - div id=(&container_id) class=[self.classes().get()] { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) } } @@ -188,17 +193,17 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del grupo de casillas. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al contenedor del grupo de casillas. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/src/base/component/form/checkbox.rs similarity index 82% rename from extensions/pagetop-bootsier/src/theme/form/checkbox.rs rename to src/base/component/form/checkbox.rs index 18ab908f..cf382e27 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/src/base/component/form/checkbox.rs @@ -1,13 +1,9 @@ -use pagetop::prelude::*; - -use crate::LOCALES_BOOTSIER; -use crate::theme::form; +use crate::prelude::*; /// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*). /// -/// Renderiza un control binario (marcado/no marcado) en dos variantes visuales, por defecto se -/// muestra como una casilla de verificación estándar, pero también puede renderizarse como un -/// interruptor de encendido/apagado ([`Checkbox::switch()`]). +/// Renderiza un control binario (marcado/no marcado) en dos variantes, por defecto como casilla de +/// verificación estándar, y también como interruptor ([`Checkbox::switch()`]). /// /// Se puede mostrar en línea con otros controles usando [`with_inline()`](Checkbox::with_inline), o /// justificar a la derecha del contenedor invirtiendo el orden de la etiqueta y el control usando @@ -15,10 +11,10 @@ use crate::theme::form; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; -/// let accept_terms = form::Checkbox::check() // También sirve new() o default(). +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let accept_terms = form::Checkbox::new() /// .with_name("terms_accepted") /// .with_label(L10n::n("I accept the terms and conditions")) /// .with_required(true); @@ -43,10 +39,8 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Checkbox { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del control. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve la variante visual del control. checkbox_kind: form::CheckboxKind, /// Devuelve el nombre del campo. @@ -73,11 +67,22 @@ impl Component for Checkbox { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - let mut classes = "form-field form-check".to_string(); + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. + let name = self + .name() + .get() + .unwrap_or_else(|| cx.required_id::(self.id(), 1)); + self.alter_name(&name); + let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor de la casilla de verificación. + let mut classes = String::from("form-field form-check"); if *self.checkbox_kind() == form::CheckboxKind::Switch { classes.push_str(" form-switch"); } @@ -87,19 +92,19 @@ impl Component for Checkbox { if *self.reverse() { classes.push_str(" form-check-reverse"); } - self.alter_classes(ClassesOp::Prepend, classes); + self.alter_prop(PropsOp::prepend_classes(classes)); } fn prepare(&self, cx: &mut Context) -> Result { - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(self.id(), 1)); - let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + let checkbox_id = util::join!(&container_id, "-checkbox"); let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch; + Ok(html! { - div id=(&container_id) class=[self.classes().get()] { + div (self.props()) { input type="checkbox" role=[is_switch.then_some("switch")] @@ -117,7 +122,7 @@ impl Component for Checkbox { @if *self.required() { span class="form-required" - title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) + title=(L10n::l("field_required").using(cx)) { "*" } @@ -145,17 +150,17 @@ impl Checkbox { // **< Checkbox BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del control. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al contenedor del control. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/src/base/component/form/component.rs similarity index 77% rename from extensions/pagetop-bootsier/src/theme/form/component.rs rename to src/base/component/form/component.rs index 80efa941..dc222ee8 100644 --- a/extensions/pagetop-bootsier/src/theme/form/component.rs +++ b/src/base/component/form/component.rs @@ -1,24 +1,22 @@ -use pagetop::prelude::*; +use crate::prelude::*; -use crate::theme::form; +use crate::base::component::form; -/// Componente para crear un **formulario** ([`form`]). +/// Componente para crear un **formulario** HTML ([`form`]). /// -/// Este componente renderiza un formulario estándar con soporte para los atributos más habituales: +/// Renderiza un formulario estándar con soporte para los atributos más habituales: /// /// - `id`: identificador opcional del formulario. /// - `classes`: clases CSS adicionales (p. ej. utilidades CSS). /// - `action`: URL/ruta de destino para el envío. -/// - `method`: método usado por el formulario para el envío de los datos (ver explicaciones en -/// [`form::Method`](crate::theme::form::Method)). +/// - `method`: método usado por el formulario para el envío de los datos (ver [`form::Method`]). /// - `accept-charset`: juego de caracteres aceptado (por defecto es `"UTF-8"`). /// - `children`: contenido del formulario. /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; -/// use pagetop_bootsier::theme::*; /// /// let form_login = Form::new() /// .with_id("login") @@ -42,15 +40,12 @@ use crate::theme::form; /// ) /// .with_child( /// Button::submit(L10n::n("Sign in")) -/// .with_color(ButtonColor::Background(Color::Primary)), /// ); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Form { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del formulario. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve la URL/ruta de destino del formulario. action: AttrValue, /// Devuelve el método para enviar el formulario. @@ -68,11 +63,11 @@ impl Component for Form { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, "form"); + self.alter_prop(PropsOp::prepend_classes("form")); } fn prepare(&self, cx: &mut Context) -> Result { @@ -82,8 +77,7 @@ impl Component for Form { }; Ok(html! { form - id=[self.id()] - class=[self.classes().get()] + (self.props()) action=[self.action().get()] method=[method] accept-charset=[self.charset().get()] @@ -97,17 +91,17 @@ impl Component for Form { impl Form { // **< Form BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del formulario. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al formulario. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/src/base/component/form/fieldset.rs similarity index 82% rename from extensions/pagetop-bootsier/src/theme/form/fieldset.rs rename to src/base/component/form/fieldset.rs index 1aacba6c..1e420480 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/src/base/component/form/fieldset.rs @@ -1,4 +1,4 @@ -use pagetop::prelude::*; +use crate::prelude::*; /// Componente para crear un **grupo de controles relacionados** en un formulario. /// @@ -13,9 +13,9 @@ use pagetop::prelude::*; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let personal_data = form::Fieldset::new() /// .with_legend(L10n::n("Personal data")) /// .with_description(L10n::n("Enter your full name and contact email.")) @@ -24,10 +24,8 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del `fieldset`. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve la leyenda del `fieldset`. legend: Attr, /// Devuelve la descripción del `fieldset`. @@ -44,7 +42,7 @@ impl Component for Fieldset { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, cx: &mut Context) -> Result { @@ -55,7 +53,7 @@ impl Component for Fieldset { } Ok(html! { - fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] { + fieldset (self.props()) disabled[*self.disabled()] { @if let Some(legend) = self.legend().lookup(cx) { legend { (legend) } } @@ -71,17 +69,17 @@ impl Component for Fieldset { impl Fieldset { // **< Fieldset BUILDER >*********************************************************************** - /// Establece el identificador único (`id`) del `fieldset` (grupo de controles). + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al `fieldset`. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/form/hidden.rs b/src/base/component/form/hidden.rs similarity index 95% rename from extensions/pagetop-bootsier/src/theme/form/hidden.rs rename to src/base/component/form/hidden.rs index 6d367155..5ad9e3ed 100644 --- a/extensions/pagetop-bootsier/src/theme/form/hidden.rs +++ b/src/base/component/form/hidden.rs @@ -1,4 +1,4 @@ -use pagetop::prelude::*; +use crate::prelude::*; /// Componente para crear un **campo oculto** del formulario. /// @@ -10,9 +10,9 @@ use pagetop::prelude::*; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let token = form::Hidden::new() /// .with_name("csrf_token") /// .with_value("a1b2c3d4e5"); diff --git a/src/base/component/form/input.rs b/src/base/component/form/input.rs new file mode 100644 index 00000000..7b57c648 --- /dev/null +++ b/src/base/component/form/input.rs @@ -0,0 +1,424 @@ +//! Definiciones para crear campos de texto de una línea. + +use crate::prelude::*; + +use std::fmt; + +// **< Kind >*************************************************************************************** + +/// Tipo de campo para un [`form::input::Field`]. +/// +/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar +/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text), +/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email), +/// [`telephone()`](Field::telephone) o [`url()`](Field::url). +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum Kind { + /// Entrada de texto genérico (`type="text"`). Es el tipo por defecto. + #[default] + Text, + /// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado. + Password, + /// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda. + Search, + /// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo. + Email, + /// Entrada de un teléfono (`type="tel"`). Activa el teclado de llamadas en móviles. + Telephone, + /// Entrada de una URL (`type="url"`). Comprueba que la entrada sea una URL bien formada. + Url, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Kind::Text => "text", + Kind::Password => "password", + Kind::Search => "search", + Kind::Email => "email", + Kind::Telephone => "tel", + Kind::Url => "url", + }) + } +} + +// **< Mode >*************************************************************************************** + +/// Sugerencia para el teclado virtual de un [`form::input::Field`]. +/// +/// Indica al navegador qué tipo de teclado virtual mostrar en dispositivos móviles o táctiles al +/// editar el campo. A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los +/// valores aceptados ni activa la validación del navegador; es sólo una sugerencia de presentación. +/// +/// Se establece con [`form::input::Field::with_inputmode()`]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Mode { + /// Suprime el teclado virtual. Útil en campos con teclado personalizado basado en JavaScript. + None, + /// Teclado de texto genérico. + Text, + /// Teclado decimal, con dígitos y separador decimal. + Decimal, + /// Teclado numérico, con sólo dígitos. + Numeric, + /// Teclado de teléfono, con dígitos y símbolos `+`, `*` y `#`. + Tel, + /// Teclado optimizado para búsquedas (puede incluir tecla de búsqueda). + Search, + /// Teclado optimizado para correo electrónico (incluye `@` y `.`). + Email, + /// Teclado optimizado para URL (incluye `/`, `.` y `.com`). + Url, +} + +impl fmt::Display for Mode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Mode::None => "none", + Mode::Text => "text", + Mode::Decimal => "decimal", + Mode::Numeric => "numeric", + Mode::Tel => "tel", + Mode::Search => "search", + Mode::Email => "email", + Mode::Url => "url", + }) + } +} + +// **< Field >************************************************************************************** + +/// Componente para crear un **campo de texto de una línea**. +/// +/// Renderiza los tipos más habituales en formularios: +/// +/// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto). +/// - [`Field::password()`]: contraseña (`type="password"`). +/// - [`Field::search()`]: búsqueda (`type="search"`). +/// - [`Field::email()`]: correo electrónico (`type="email"`). +/// - [`Field::telephone()`]: teléfono (`type="tel"`). +/// - [`Field::url()`]: URL (`type="url"`). +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let email = form::input::Field::email() +/// .with_name("email") +/// .with_label(L10n::n("Email address")) +/// .with_placeholder(L10n::n("user@example.com")) +/// .with_autocomplete(Some(form::Autocomplete::email())) +/// .with_required(true); +/// ``` +/// +/// Al enviar el formulario el navegador transmite `name=valor`. Un campo de texto siempre envía su +/// valor, incluso si está vacío. En el servidor se deserializa como `String`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// email: String, // Siempre presente; cadena vacía si el usuario no escribió nada. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Field { + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, + /// Devuelve el tipo de campo. + kind: Kind, + /// Devuelve el nombre del campo. + name: AttrName, + /// Devuelve el valor inicial del campo. + value: AttrValue, + /// Devuelve la etiqueta del campo. + label: Attr, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve la longitud mínima permitida en caracteres. + minlength: Attr, + /// Devuelve la longitud máxima permitida en caracteres. + maxlength: Attr, + /// Devuelve el texto indicativo del campo. + placeholder: Attr, + /// Devuelve la configuración de autocompletado del campo. + autocomplete: Attr, + /// Devuelve si el campo recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si el campo es de sólo lectura. + readonly: bool, + /// Devuelve si el campo es obligatorio. + required: bool, + /// Devuelve si el campo está deshabilitado. + disabled: bool, + /// Devuelve si el campo se muestra como texto plano sin bordes ni fondo. + plaintext: bool, + /// Devuelve la sugerencia de teclado virtual para el campo. + inputmode: Attr, +} + +impl Component for Field { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + } + + // Clases CSS del contenedor del campo de texto. + self.alter_prop(PropsOp::prepend_classes(util::join!( + "form-field form-field-", + self.kind().to_string() + ))); + } + + fn prepare(&self, cx: &mut Context) -> Result { + let container_id = self.id(); + let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); + let input_class = if *self.plaintext() { + "form-control-plaintext" + } else { + "form-control" + }; + + Ok(html! { + div (self.props()) { + @if let Some(label) = self.label().lookup(cx) { + label for=[input_id.as_deref()] class="form-label" { + (label) + @if *self.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + } + input + type=(self.kind()) + id=[input_id.as_deref()] + class=(input_class) + name=[self.name().get()] + value=[self.value().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] + placeholder=[self.placeholder().lookup(cx)] + inputmode=[self.inputmode().get()] + autocomplete=[self.autocomplete().get()] + autofocus[*self.autofocus()] + readonly[*self.readonly() || *self.plaintext()] + required[*self.required()] + disabled[*self.disabled()]; + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Field { + /// Crea un campo de **texto genérico** (`type="text"`). + /// + /// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada + /// textual sin restricciones de formato específicas. + pub fn text() -> Self { + Self::default() + } + + /// Crea un campo de **contraseña** (`type="password"`). + /// + /// El navegador oculta los caracteres introducidos. Se recomienda usar con + /// [`with_autocomplete()`](Self::with_autocomplete) para permitir autorrellenar con una + /// contraseña guardada o dejar al usuario recibir sugerencias o crear una nueva. + pub fn password() -> Self { + Self { + kind: Kind::Password, + ..Default::default() + } + } + + /// Crea un campo de **búsqueda** (`type="search"`). + /// + /// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos navegadores + /// añaden un botón para borrar el contenido. + pub fn search() -> Self { + Self { + kind: Kind::Search, + ..Default::default() + } + } + + /// Crea un campo de **correo electrónico** (`type="email"`). + /// + /// El navegador valida el formato de la dirección antes de enviar el formulario. En + /// dispositivos móviles muestra un teclado adaptado para introducir direcciones de correo. + pub fn email() -> Self { + Self { + kind: Kind::Email, + ..Default::default() + } + } + + /// Crea un campo de **teléfono** (`type="tel"`). + /// + /// No impone ninguna restricción de formato (los formatos de teléfono varían por país), pero + /// en dispositivos móviles muestra el teclado numérico de llamadas. + pub fn telephone() -> Self { + Self { + kind: Kind::Telephone, + ..Default::default() + } + } + + /// Crea un campo de **URL** (`type="url"`). + /// + /// El navegador valida que el valor sea una URL bien formada antes de enviar el formulario. + pub fn url() -> Self { + Self { + kind: Kind::Url, + ..Default::default() + } + } + + // **< Field BUILDER >************************************************************************** + + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); + self + } + + /// Establece el nombre del campo (atributo `name`). + /// + /// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para + /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + /// Establece el valor inicial del campo. + #[builder_fn] + pub fn with_value(mut self, value: impl AsRef) -> Self { + self.value.alter_str(value); + self + } + + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + #[builder_fn] + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + #[builder_fn] + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); + self + } + + /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). + #[builder_fn] + pub fn with_minlength(mut self, minlength: Option) -> Self { + self.minlength.alter_opt(minlength); + self + } + + /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). + #[builder_fn] + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); + self + } + + /// Establece o elimina el texto indicativo del campo (`None` para quitarlo). + /// + /// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir. + /// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo. + #[builder_fn] + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); + self + } + + /// Establece la configuración de autocompletado del campo. + /// + /// Usar los métodos de [`form::Autocomplete`] para los valores más habituales (p. ej. + /// [`Autocomplete::email()`](form::Autocomplete::email) o + /// [`Autocomplete::current_password()`](form::Autocomplete::current_password)). + #[builder_fn] + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); + self + } + + /// Establece si el campo recibe el foco automáticamente al cargar la página. + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + /// Establece si el campo es de sólo lectura. + #[builder_fn] + pub fn with_readonly(mut self, readonly: bool) -> Self { + self.readonly = readonly; + self + } + + /// Establece si el campo es obligatorio. + #[builder_fn] + pub fn with_required(mut self, required: bool) -> Self { + self.required = required; + self + } + + /// Establece si el campo está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + /// Establece si el campo se muestra como texto plano (sin bordes ni fondo). + /// + /// Útil para mostrar un valor no editable en pantalla que sí se envía al servidor con el + /// formulario. El efecto visual depende del tema activo. + #[builder_fn] + pub fn with_plaintext(mut self, plaintext: bool) -> Self { + self.plaintext = plaintext; + self + } + + /// Establece el modo de entrada sugerido para el teclado virtual en dispositivos móviles. + /// + /// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados + /// ni activa la validación del navegador; es sólo una sugerencia de presentación. + #[builder_fn] + pub fn with_inputmode(mut self, inputmode: Option) -> Self { + self.inputmode.alter_opt(inputmode); + self + } +} diff --git a/extensions/pagetop-bootsier/src/theme/form/props.rs b/src/base/component/form/props.rs similarity index 97% rename from extensions/pagetop-bootsier/src/theme/form/props.rs rename to src/base/component/form/props.rs index 2ef88f3e..4e69d924 100644 --- a/extensions/pagetop-bootsier/src/theme/form/props.rs +++ b/src/base/component/form/props.rs @@ -1,20 +1,21 @@ -use pagetop::prelude::*; +use crate::prelude::*; use std::borrow::Cow; use std::fmt; // **< CheckboxKind >******************************************************************************* -/// Variante visual para [`form::Checkbox`](crate::theme::form::Checkbox) en un formulario. +/// Variante visual para un [`form::Checkbox`] en un formulario. /// /// Determina si el control se renderiza como una casilla de verificación estándar o como un -/// interruptor (*toggle switch*). +/// interruptor (*toggle switch*). La variante [`Switch`](Self::Switch) añade la clase `form-switch` +/// al contenedor y el atributo `role="switch"` al control para accesibilidad. #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub enum CheckboxKind { /// Casilla de verificación estándar. Es el tipo por defecto. #[default] Check, - /// Interruptor de encendido/apagado. + /// Interruptor de encendido/apagado (*toggle switch*). Switch, // TODO: Añadir variante `NativeSwitch` cuando el atributo `switch` de la propuesta WHATWG // (https://github.com/whatwg/html/issues/9546) sea estándar y tenga soporte amplio. Safari ya @@ -50,9 +51,9 @@ pub enum CheckboxKind { /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// // Correo electrónico con sugerencia semántica del navegador. /// let ac = form::Autocomplete::email(); /// @@ -87,7 +88,7 @@ impl Autocomplete { // --< Secciones >------------------------------------------------------------------------------ /// Construye `autocomplete` con un prefijo de sección y un token o tokens del - /// [`form::AutofillField`](AutofillField) indicado. + /// [`AutofillField`] indicado. /// /// Genera `autocomplete="section- "`. Si `name` no es ASCII o contiene espacios, /// se ignora la sección y se genera sólo el token indicado. @@ -243,8 +244,9 @@ impl fmt::Display for Autocomplete { /// /// # Ejemplo /// -/// ```rust -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let ac = form::Autocomplete::token(form::AutofillField::Username); /// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress); /// let ac = form::Autocomplete::section("job", form::AutofillField::Email); @@ -447,7 +449,7 @@ impl AutofillField { // **< Method >************************************************************************************* -/// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos. +/// Método HTTP usado por un [`Form`](super::Form) para el envío de los datos. /// /// En HTML, el atributo `method` del formulario indica **cómo** se envían los datos: /// diff --git a/extensions/pagetop-bootsier/src/theme/form/radio.rs b/src/base/component/form/radio.rs similarity index 85% rename from extensions/pagetop-bootsier/src/theme/form/radio.rs rename to src/base/component/form/radio.rs index d79c4f44..f06dd387 100644 --- a/extensions/pagetop-bootsier/src/theme/form/radio.rs +++ b/src/base/component/form/radio.rs @@ -1,12 +1,10 @@ //! Definiciones para crear grupos de botones de opción (*radio buttons*). -use pagetop::prelude::*; - -use crate::LOCALES_BOOTSIER; +use crate::prelude::*; // **< Item >*************************************************************************************** -/// Botón de opción individual de un [`form::radio::Field`](Field). +/// Botón de opción individual de un [`Field`]. /// /// Representa cada opción de un grupo de opciones exclusivas entre sí, con un valor (el que se /// envía al servidor), una etiqueta localizable visible y puede marcarse como seleccionada o @@ -14,9 +12,9 @@ use crate::LOCALES_BOOTSIER; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] @@ -64,8 +62,8 @@ impl Item { /// Componente para crear un **grupo de botones de opción**. /// -/// Renderiza un grupo de botones de opción [`form::radio::Item`](Item) que comparten el mismo -/// atributo `name`, por lo que sólo puede seleccionarse uno a la vez. Las opciones se añaden con +/// Renderiza un grupo de botones de opción [`form::radio::Item`] que comparten el mismo atributo +/// `name`, por lo que sólo puede seleccionarse uno a la vez. Las opciones se añaden con /// [`with_item()`](Field::with_item). /// /// Si se activa el modo en línea [`with_inline()`](Field::with_inline), los botones se disponen @@ -74,9 +72,9 @@ impl Item { /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let plan = form::radio::Field::new() /// .with_name("plan") /// .with_label(L10n::n("Subscription plan")) @@ -96,10 +94,8 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del grupo. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el nombre compartido por todos los botones de opción del grupo. name: AttrName, /// Devuelve la etiqueta del grupo. @@ -122,28 +118,38 @@ impl Component for Field { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } - fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, "form-field form-field-radios"); - } - - fn prepare(&self, cx: &mut Context) -> Result { + fn setup(&mut self, cx: &Context) { + // Asegura `name` e `id`. + // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. let name = self .name() .get() .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + self.alter_name(&name); let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); + self.alter_prop(PropsOp::ensure_id(container_id)); + + // Clases CSS del contenedor del grupo de opciones. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. + let name = self.name().get().unwrap(); + let container_id = self.id().unwrap(); + Ok(html! { - div id=(&container_id) class=[self.classes().get()] { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) @if *self.required() { span class="form-required" - title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) + title=(L10n::l("field_required").using(cx)) { "*" } @@ -190,17 +196,17 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del grupo de opciones. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al contenedor del grupo de opciones. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/extensions/pagetop-bootsier/src/theme/form/range.rs b/src/base/component/form/range.rs similarity index 86% rename from extensions/pagetop-bootsier/src/theme/form/range.rs rename to src/base/component/form/range.rs index 45d07acf..386768d7 100644 --- a/extensions/pagetop-bootsier/src/theme/form/range.rs +++ b/src/base/component/form/range.rs @@ -1,4 +1,4 @@ -use pagetop::prelude::*; +use crate::prelude::*; /// Componente para crear un **control deslizante** de rango. /// @@ -8,9 +8,9 @@ use pagetop::prelude::*; /// /// # Ejemplo /// -/// ```rust -/// # use pagetop::prelude::*; -/// # use pagetop_bootsier::theme::*; +/// ```rust,no_run +/// use pagetop::prelude::*; +/// /// let volume = form::Range::new() /// .with_name("volume") /// .with_label(L10n::n("Volume")) @@ -31,10 +31,8 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Range { - #[getters(skip)] - id: AttrId, - /// Devuelve las clases CSS del contenedor del control deslizante. - classes: Classes, + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, /// Devuelve el nombre del campo. name: AttrName, /// Devuelve la etiqueta del campo. @@ -61,20 +59,26 @@ impl Component for Range { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn setup(&mut self, _cx: &Context) { - self.alter_classes(ClassesOp::Prepend, "form-field form-field-range"); + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + }; + + // Clases CSS del contenedor del control deslizante. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-range")); } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let container_id = self.id(); let range_id = container_id.as_deref().map(|id| util::join!(id, "-range")); Ok(html! { - div id=[container_id.as_deref()] class=[self.classes().get()] { + div (self.props()) { @if let Some(label) = self.label().lookup(cx) { label for=[range_id.as_deref()] class="form-label" { (label) } } @@ -100,17 +104,17 @@ impl Component for Range { impl Range { // **< Range BUILDER >************************************************************************** - /// Establece el identificador único (`id`) del contenedor del control deslizante. + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - self.id.alter_id(id); + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); self } - /// Modifica la lista de clases CSS aplicadas al contenedor del control deslizante. + /// Modifica identificador, clases CSS o atributos HTML del componente. #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.classes.alter_classes(op, classes); + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); self } diff --git a/src/base/component/form/select.rs b/src/base/component/form/select.rs new file mode 100644 index 00000000..fc94d06c --- /dev/null +++ b/src/base/component/form/select.rs @@ -0,0 +1,427 @@ +//! Definiciones para crear listas de selección. + +use crate::prelude::*; + +// **< Item >*************************************************************************************** + +/// Elemento individual de [`form::select::Field`] o de [`form::select::Group`]. +/// +/// Representa un elemento dentro de una lista de selección o de un grupo de elementos de la lista. +/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el +/// usuario. +/// +/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Self::with_selected) o +/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Self::with_disabled). +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true); +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Item { + /// Devuelve el valor enviado al servidor cuando se selecciona el elemento. + value: AttrValue, + /// Devuelve la etiqueta visible del elemento. + label: L10n, + /// Devuelve si el elemento debe aparecer seleccionado por defecto. + selected: bool, + /// Devuelve si el elemento está deshabilitado. + disabled: bool, +} + +impl Item { + /// Crea un nuevo elemento con el valor y la etiqueta indicados. + pub fn new(value: impl AsRef, label: L10n) -> Self { + Self { + value: AttrValue::new(value), + label, + selected: false, + disabled: false, + } + } + + // **< Item BUILDER >*************************************************************************** + + /// Establece si el elemento aparece seleccionado por defecto. + /// + /// En una lista de selección única, el navegador aplica la selección al último elemento marcado + /// si hay más de uno; mientras que en una lista múltiple se respetan todos los elementos + /// marcados. + pub fn with_selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + /// Establece si el elemento está deshabilitado. + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +// **< Group >************************************************************************************** + +/// Grupo de elementos dentro de [`form::select::Field`]. +/// +/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El +/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Self::with_disabled). +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let group = form::select::Group::new(L10n::n("Europe")) +/// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) +/// .with_item(form::select::Item::new("fr", L10n::n("French"))); +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Group { + /// Devuelve la etiqueta visible del grupo de elementos. + label: L10n, + /// Devuelve los elementos del grupo. + items: Vec, + /// Devuelve si el grupo de elementos está deshabilitado. + disabled: bool, +} + +impl Group { + /// Crea un nuevo grupo con la etiqueta indicada. + pub fn new(label: L10n) -> Self { + Self { + label, + ..Self::default() + } + } + + // **< Group BUILDER >************************************************************************** + + /// Añade un elemento al grupo. Los elementos se muestran en el orden en que se añaden. + pub fn with_item(mut self, item: Item) -> Self { + self.items.push(item); + self + } + + /// Establece si el grupo de elementos está deshabilitado en bloque. + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +// **< Entry >************************************************************************************** + +/// Entrada de [`form::select::Field`] con un elemento o un grupo de elementos. +/// +/// Cada entrada se crea implícitamente cuando se usa [`form::select::Field::with_item()`] para +/// añadir un elemento individual o [`form::select::Field::with_group()`] para añadir un grupo de +/// elementos a una lista de selección. +/// +/// Con [`form::select::Field::entries()`] se pueden recuperar todas las entradas para su +/// renderizado. +#[derive(Clone, Debug)] +pub enum Entry { + /// Elemento individual. + Item(Item), + /// Grupo de elementos. + Group(Group), +} + +// **< Field >************************************************************************************** + +/// Componente para crear una **lista de selección**. +/// +/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir +/// uno, o más de uno si se activa la selección múltiple con +/// [`with_multiple()`](Self::with_multiple). +/// +/// Los elementos individuales se añaden con [`with_item()`](Self::with_item); los grupos de +/// elementos con un encabezado común se añaden con [`with_group()`](Self::with_group). Ambos +/// métodos pueden combinarse libremente. +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let idioma = form::select::Field::new() +/// .with_name("language") +/// .with_label(L10n::n("Language")) +/// .with_item(form::select::Item::new("", L10n::n("— Choose —")).with_selected(true)) +/// .with_group( +/// form::select::Group::new(L10n::n("Europe")) +/// .with_item(form::select::Item::new("es", L10n::n("Spanish"))) +/// .with_item(form::select::Item::new("fr", L10n::n("French"))), +/// ) +/// .with_group( +/// form::select::Group::new(L10n::n("Americas")) +/// .with_item(form::select::Item::new("en", L10n::n("English"))) +/// .with_item(form::select::Item::new("pt", L10n::n("Portuguese"))), +/// ) +/// .with_required(true); +/// ``` +/// +/// Cuando el usuario selecciona un elemento y envía el formulario, el navegador transmite +/// `name=valor`. Si el campo es obligatorio el valor siempre estará presente y puede deserializarse +/// como `String`; si es opcional, usa `Option`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// language: String, // Siempre presente (campo obligatorio). +/// // language: Option, // None si no se selecciona ninguna opción. +/// } +/// ``` +/// +/// Con selección múltiple activa, el navegador envía un valor por cada elemento marcado; si no se +/// marca ninguno, no envía nada. Usa `Vec` con `#[serde(default)]`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// #[serde(default)] +/// interests: Vec, // p. ej. ["art", "tech"] o [] si no se marcó ninguna. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Field { + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, + /// Devuelve el nombre del campo. + name: AttrName, + /// Devuelve la etiqueta del campo. + label: Attr, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve las entradas de la lista (elementos individuales y grupos de elementos). + entries: Vec, + /// Devuelve si la lista permite selección múltiple. + multiple: bool, + /// Devuelve el número de filas visibles de la lista de selección. + rows: Attr, + /// Devuelve la configuración de autocompletado del campo. + autocomplete: Attr, + /// Devuelve si la lista recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si la selección de un elemento es obligatoria. + required: bool, + /// Devuelve si la lista está deshabilitada. + disabled: bool, +} + +impl Component for Field { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + } + + // Clases CSS del contenedor de la lista de selección. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-select")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + let container_id = self.id(); + let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); + + Ok(html! { + div (self.props()) { + @if let Some(label) = self.label().lookup(cx) { + label for=[select_id.as_deref()] class="form-label" { + (label) + @if *self.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + } + select + id=[select_id.as_deref()] + class="form-select" + name=[self.name().get()] + multiple[*self.multiple()] + size=[self.rows().get()] + autocomplete=[self.autocomplete().get()] + autofocus[*self.autofocus()] + required[*self.required()] + disabled[*self.disabled()] + { + @for entry in self.entries() { + @match entry { + Entry::Item(opt) => { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + Entry::Group(group) => { + optgroup + label=(group.label().using(cx)) + disabled[*group.disabled()] + { + @for opt in group.items() { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + } + } + } + } + } + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Field { + // **< Field BUILDER >************************************************************************** + + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); + self + } + + /// Establece el nombre del campo (atributo `name`). + /// + /// Sin él, el valor seleccionado no se transmite al servidor al enviar el formulario. Para + /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + #[builder_fn] + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + #[builder_fn] + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); + self + } + + /// Añade un elemento individual a la lista de selección. + /// + /// Los elementos y grupos se muestran en el orden en que se añaden. + #[builder_fn] + pub fn with_item(mut self, item: Item) -> Self { + self.entries.push(Entry::Item(item)); + self + } + + /// Añade un grupo de elementos a la lista de selección. + /// + /// Los elementos y grupos se muestran en el orden en que se añaden. + #[builder_fn] + pub fn with_group(mut self, group: Group) -> Self { + self.entries.push(Entry::Group(group)); + self + } + + /// Establece si el control permite seleccionar varios elementos. + /// + /// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se + /// recomienda combinar con [`with_rows()`](Self::with_rows) para controlar el número de filas + /// visibles. + /// + /// Para un número reducido de elementos con etiquetas descriptivas considera usar + /// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en + /// pantallas pequeñas. + #[builder_fn] + pub fn with_multiple(mut self, multiple: bool) -> Self { + self.multiple = multiple; + self + } + + /// Establece el número de filas visibles de la lista de selección. + /// + /// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de + /// desplegable, tanto en modo simple como múltiple. Con `None` se omite el atributo y presenta + /// el control como desplegable (comportamiento por defecto). + /// + /// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin + /// necesidad de recurrir al desplazamiento. + #[builder_fn] + pub fn with_rows(mut self, rows: Option) -> Self { + self.rows.alter_opt(rows); + self + } + + /// Establece la configuración de autocompletado del campo. + /// + /// Permite al navegador rellenar automáticamente el elemento seleccionado en listas de países + /// (`"country"`), idiomas (`"language"`), sexo (`"sex"`) u otros campos con valores + /// predefinidos. En listas de selección múltiples no es útil en la práctica, ya que los + /// navegadores no gestionan selecciones múltiples con autocompletado. + /// + /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para + /// omitir el atributo. + #[builder_fn] + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); + self + } + + /// Establece si el campo recibe el foco automáticamente al cargar la página. + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + /// Establece si el campo es obligatorio. + #[builder_fn] + pub fn with_required(mut self, required: bool) -> Self { + self.required = required; + self + } + + /// Establece si el campo está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} diff --git a/src/base/component/form/textarea.rs b/src/base/component/form/textarea.rs new file mode 100644 index 00000000..5b4cc1ff --- /dev/null +++ b/src/base/component/form/textarea.rs @@ -0,0 +1,256 @@ +//! Definiciones para crear áreas de texto en formularios. + +use crate::prelude::*; + +/// Componente para crear un **área de texto** de formulario. +/// +/// Permite escribir en un área de texto de más de una línea, con una etiqueta opcional y atributos +/// como el número de filas a presentar, longitud mínima (`minlength`) y máxima (`maxlength`), texto +/// indicativo (`placeholder`) o autocompletado (`autocomplete`). +/// +/// # Ejemplo +/// +/// ```rust,no_run +/// use pagetop::prelude::*; +/// +/// let descripcion = form::Textarea::new() +/// .with_name("description") +/// .with_label(L10n::n("Description")) +/// .with_rows(Some(8)) +/// .with_maxlength(Some(500)) +/// .with_placeholder(L10n::n("Write here...")) +/// .with_required(true); +/// ``` +/// +/// Al enviar el formulario el navegador transmite `name=valor`. Un área de texto siempre envía su +/// valor, incluso si está vacía. En el servidor se deserializa como `String`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// description: String, // Siempre presente; cadena vacía si el usuario no escribió nada. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Textarea { + /// Devuelve identificador, clases CSS y atributos HTML del componente. + props: Props, + /// Devuelve el nombre del campo. + name: AttrName, + /// Devuelve el valor inicial del área de texto. + value: AttrValue, + /// Devuelve la etiqueta del campo. + label: Attr, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve el número de filas visibles del área de texto. + rows: Attr, + /// Devuelve la longitud mínima permitida en caracteres. + minlength: Attr, + /// Devuelve la longitud máxima permitida en caracteres. + maxlength: Attr, + /// Devuelve el texto indicativo del área de texto. + placeholder: Attr, + /// Devuelve la configuración de autocompletado del campo. + autocomplete: Attr, + /// Devuelve si el campo recibe el foco automáticamente al cargar la página. + autofocus: bool, + /// Devuelve si el campo es de sólo lectura. + readonly: bool, + /// Devuelve si el campo es obligatorio. + required: bool, + /// Devuelve si el campo está deshabilitado. + disabled: bool, +} + +impl Component for Textarea { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.props.get_id() + } + + fn setup(&mut self, _cx: &Context) { + if let Some(container_id) = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) + { + self.alter_prop(PropsOp::ensure_id(container_id)); + } + + // Clases CSS del contenedor del área de texto. + self.alter_prop(PropsOp::prepend_classes("form-field form-field-textarea")); + } + + fn prepare(&self, cx: &mut Context) -> Result { + let container_id = self.id(); + let textarea_id = container_id + .as_deref() + .map(|id| util::join!(id, "-textarea")); + + Ok(html! { + div (self.props()) { + @if let Some(label) = self.label().lookup(cx) { + label for=[textarea_id.as_deref()] class="form-label" { + (label) + @if *self.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + } + textarea + id=[textarea_id.as_deref()] + class="form-control" + name=[self.name().get()] + rows=[self.rows().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] + placeholder=[self.placeholder().lookup(cx)] + autocomplete=[self.autocomplete().get()] + autofocus[*self.autofocus()] + readonly[*self.readonly()] + required[*self.required()] + disabled[*self.disabled()] + { + @if let Some(value) = self.value().get() { + (value) + } + } + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Textarea { + // **< Textarea BUILDER >*********************************************************************** + + /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.props.alter_id(id); + self + } + + /// Modifica identificador, clases CSS o atributos HTML del componente. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + self.props.alter_prop(op); + self + } + + /// Establece el nombre del campo (atributo `name`). + /// + /// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para + /// deserializar el campo en el servidor es recomendable establecer un `name` explícito. + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + /// Establece el valor inicial del área de texto. + #[builder_fn] + pub fn with_value(mut self, value: impl AsRef) -> Self { + self.value.alter_str(value); + self + } + + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). + #[builder_fn] + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). + #[builder_fn] + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); + self + } + + /// Establece el número de filas visibles del área de texto. + /// + /// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el + /// estándar. + #[builder_fn] + pub fn with_rows(mut self, rows: Option) -> Self { + self.rows.alter_opt(rows); + self + } + + /// Establece la longitud mínima permitida en caracteres. + #[builder_fn] + pub fn with_minlength(mut self, minlength: Option) -> Self { + self.minlength.alter_opt(minlength); + self + } + + /// Establece la longitud máxima permitida en caracteres. + #[builder_fn] + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); + self + } + + /// Establece o elimina el texto indicativo del área de texto (`None` para quitarlo). + /// + /// Este texto aparece en el área de texto y desaparece en cuanto el usuario empieza a escribir. + /// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo. + #[builder_fn] + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); + self + } + + /// Establece la configuración de autocompletado del campo. + /// + /// Permite al navegador sugerir o rellenar automáticamente el contenido del área de texto + /// con valores guardados. Es especialmente útil en áreas con contenido semántico predefinido. + /// + /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para + /// omitir el atributo. + #[builder_fn] + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); + self + } + + /// Establece si el campo recibe el foco automáticamente al cargar la página. + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + /// Establece si el campo es de sólo lectura. + #[builder_fn] + pub fn with_readonly(mut self, readonly: bool) -> Self { + self.readonly = readonly; + self + } + + /// Establece si el campo es obligatorio. + #[builder_fn] + pub fn with_required(mut self, required: bool) -> Self { + self.required = required; + self + } + + /// Establece si el campo está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} diff --git a/src/base/component/html.rs b/src/base/component/html.rs index 9f44be6b..e0c9e519 100644 --- a/src/base/component/html.rs +++ b/src/base/component/html.rs @@ -10,7 +10,7 @@ use std::sync::Arc; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let component = Html::with(|_| { /// html! { @@ -23,7 +23,7 @@ use std::sync::Arc; /// /// Para renderizar contenido que dependa del contexto, se puede acceder a él dentro del *closure*: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let component = Html::with(|cx| { /// let user = cx.param_or("username", "visitor".to_string()); diff --git a/src/base/component/intro.rs b/src/base/component/intro.rs index 63902f10..1ecd6318 100644 --- a/src/base/component/intro.rs +++ b/src/base/component/intro.rs @@ -34,14 +34,14 @@ pub enum IntroOpening { /// /// **Intro mínima por defecto** /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default(); /// ``` /// /// **Título, eslogan y botón personalizados** /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default() /// .with_title(L10n::l("intro_custom_title")) @@ -54,7 +54,7 @@ pub enum IntroOpening { /// /// **Sin botón y en modo *Custom* (sin *badges* predefinidos)** /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default() /// .with_button(None::<(L10n, FnPathByContext)>) @@ -63,7 +63,7 @@ pub enum IntroOpening { /// /// **Añadir contenidos hijo** /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default() /// .with_child( @@ -221,7 +221,7 @@ impl Intro { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default().with_title(L10n::n("Intro title")); /// ``` @@ -235,7 +235,7 @@ impl Intro { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default().with_slogan(L10n::n("A short slogan")); /// ``` @@ -253,7 +253,7 @@ impl Intro { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// // Define un botón con texto y una URL fija. /// let intro = Intro::default().with_button(Some((L10n::n("Start"), |_| "/start".into()))); @@ -274,7 +274,7 @@ impl Intro { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let intro = Intro::default().with_opening(IntroOpening::Custom); /// ``` diff --git a/src/base/component/poweredby.rs b/src/base/component/poweredby.rs index bd2e0a9b..3e4792b8 100644 --- a/src/base/component/poweredby.rs +++ b/src/base/component/poweredby.rs @@ -47,7 +47,7 @@ impl PoweredBy { /// Al pasar `Some(valor)` se sobrescribe el texto de copyright por defecto. Al pasar `None` se /// eliminará, pero en este caso es necesario especificar el tipo explícitamente: /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let p1 = PoweredBy::default().with_copyright(Some("2001 © Foo Inc.")); /// let p2 = PoweredBy::new().with_copyright(None::); diff --git a/src/core/component.rs b/src/core/component.rs index 93421e13..54a00b36 100644 --- a/src/core/component.rs +++ b/src/core/component.rs @@ -27,7 +27,7 @@ pub use context::{AssetsOp, Context, ContextError, Contextual}; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// #[derive(AutoDefault, Clone)] /// struct SampleComponent { @@ -81,7 +81,7 @@ pub type FnIsRenderable = fn(cx: &Context) -> bool; /// El caso más común es construir rutas relativas dependientes del contexto, normalmente usando /// [`Context::route`](crate::core::component::Context::route): /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # let relative_route: FnPathByContext = /// |cx| cx.route("/path/to/page") @@ -90,7 +90,7 @@ pub type FnIsRenderable = fn(cx: &Context) -> bool; /// /// También es posible usar rutas estáticas sin asignaciones adicionales: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # let external_route: FnPathByContext = /// |_| "https://www.example.com".into() @@ -99,7 +99,7 @@ pub type FnIsRenderable = fn(cx: &Context) -> bool; /// /// O componer rutas dinámicas en tiempo de ejecución: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # let dynamic_route: FnPathByContext = /// |cx| RoutePath::new("/user").with_param("id", cx.param::("user_id").unwrap().to_string()) diff --git a/src/core/component/children.rs b/src/core/component/children.rs index bfedec14..480170f4 100644 --- a/src/core/component/children.rs +++ b/src/core/component/children.rs @@ -178,7 +178,7 @@ impl Embed { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let embed = Embed::with(Html::with(|_| html! { "Prueba" })); /// { diff --git a/src/core/component/context.rs b/src/core/component/context.rs index 9ff7251c..d3b13cd3 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -3,14 +3,14 @@ use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage}; use crate::core::theme::all::DEFAULT_THEME; use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{Markup, RoutePath, html}; +use crate::html::{Markup, Props, PropsOp, RoutePath, html}; use crate::locale::L10n; use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::web::HttpRequest; use crate::{CowStr, builder_fn, util}; -use std::any::Any; -use std::cell::Cell; +use std::any::{Any, TypeId}; +use std::cell::RefCell; use std::collections::HashMap; use std::fmt; @@ -83,7 +83,7 @@ impl std::error::Error for ContextError {} /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_aliner::Aliner; /// fn prepare_context(cx: C) -> C { @@ -123,7 +123,7 @@ pub trait Contextual: LangId { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let cx = Context::new(None) /// .with_param("user_id", 42_i32) @@ -137,6 +137,10 @@ pub trait Contextual: LangId { #[builder_fn] fn with_assets(self, op: AssetsOp) -> Self; + /// Modifica identificador, clases CSS o atributos HTML del elemento ``. + #[builder_fn] + fn with_body_props(self, op: PropsOp) -> Self; + /// Añade un componente o aplica una operación [`ChildOp`] en la región por defecto del /// documento. #[builder_fn] @@ -206,6 +210,9 @@ pub trait Contextual: LangId { /// Devuelve los scripts JavaScript de los recursos del contexto. fn javascripts(&self) -> &Assets; + /// Devuelve identificador, clases CSS y atributos HTML del elemento ``. + fn body_props(&self) -> &Props; + // **< Contextual HELPERS >********************************************************************* /// Elimina un parámetro del contexto. Devuelve `true` si la clave existía y se eliminó. @@ -232,7 +239,7 @@ pub trait Contextual: LangId { /// /// Crea un nuevo contexto asociado a una petición HTTP: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # use pagetop_aliner::Aliner; /// fn new_context(request: HttpRequest) -> Context { @@ -254,7 +261,7 @@ pub trait Contextual: LangId { /// /// Y hace operaciones con un contexto dado: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # #[derive(AutoDefault, Clone, Debug)] /// # struct Menu; @@ -271,7 +278,7 @@ pub trait Contextual: LangId { /// assert_eq!(id, 42); /// /// // Genera un identificador para un componente de tipo `Menu`. -/// let unique_id = cx.required_id::

(None, 1); +/// let unique_id = cx.build_id::(1); /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. /// } /// ``` @@ -284,9 +291,10 @@ pub struct Context { favicon : Option, // Favicon, si se ha definido. stylesheets: Assets, // Hojas de estilo CSS. javascripts: Assets, // Scripts JavaScript. + body_props : Props, // Identificador, clases CSS y atributos del . regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. - id_counter : Cell, // Cell permite incrementar desde &self en required_id(). + id_counters: RefCell>, // RefCell permite mutar desde build_id(&self). messages : Vec, // Mensajes de usuario acumulados. } @@ -312,9 +320,10 @@ impl Context { favicon : None, stylesheets: Assets::::new(), javascripts: Assets::::new(), + body_props : Props::default(), regions : ChildrenInRegions::default(), params : HashMap::default(), - id_counter : Cell::new(0), + id_counters: RefCell::new(HashMap::new()), messages : Vec::new(), } } @@ -374,31 +383,42 @@ impl Context { route } - /// Garantiza un identificador único para un componente `C`, generándolo si no se proporciona - /// ninguno. + /// Construye un identificador HTML único para el tipo de componente `C`. /// - /// Si `id` es `None`, crea un identificador usando los últimos segmentos del *path* completo - /// del tipo `C`, separados por `-` y en minúsculas, seguidos de un contador incremental interno - /// del contexto. Por ejemplo, para un componente `MyApp::ui::Menu` con `parts = 2` podría - /// devolver un identificador como `ui-menu-1` si ha sido el primero en generarse. + /// Toma los `segments` finales del *path* completo del tipo, los une con `-` y los convierte a + /// minúsculas, y añade un contador independiente por tipo. Por ejemplo, para `MyApp::ui::Menu` + /// con `segments = 2` devuelve `ui-menu-1` la primera vez que se invoca para ese tipo, + /// `ui-menu-2` la segunda, etc. /// - /// Con `parts = 1` se usa el nombre corto del tipo. Si `parts` es `0` o supera el número de - /// segmentos del *path*, entonces se usará el *path* completo. + /// Con `segments = 1` se usa sólo el nombre corto del tipo. Si `segments` es `0` o supera el + /// número de segmentos del *path*, se usan todos. /// - /// Es útil para asignar identificadores HTML cuando el componente no recibe uno explícito. - pub fn required_id(&self, id: Option, parts: usize) -> String { - if let Some(id) = id { - return id; - } - let segments: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); - let parts = if parts == 0 || parts >= segments.len() { - segments.len() + /// Es útil para asignar identificadores cuando el componente no recibe uno explícito. El + /// contador es local a este contexto y se reinicia para cada nueva petición. + pub fn build_id(&self, segments: usize) -> String { + let path: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); + let segments = if segments == 0 || segments >= path.len() { + path.len() } else { - parts + segments }; - self.id_counter.set(self.id_counter.get() + 1); - let prefix = segments[segments.len() - parts..].join("-").to_lowercase(); - util::join!(prefix, "-", self.id_counter.get().to_string()) + let count = { + let mut map = self.id_counters.borrow_mut(); + let n = map.entry(TypeId::of::()).or_insert(0); + *n += 1; + *n + }; + let prefix = path[path.len() - segments..].join("-").to_lowercase(); + util::join!(prefix, "-", count.to_string()) + } + + /// Devuelve `id` si contiene un valor, o genera uno único con [`build_id`](Self::build_id) + /// si es `None`. + pub fn required_id(&self, id: Option, segments: usize) -> String { + match id { + Some(id) => id, + None => self.build_id::(segments), + } } /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. @@ -409,7 +429,7 @@ impl Context { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// # let mut cx = Context::new(None); /// cx.push_message(MessageLevel::Warning, L10n::n("Session is not valid")); @@ -510,6 +530,12 @@ impl Contextual for Context { self } + #[builder_fn] + fn with_body_props(mut self, op: PropsOp) -> Self { + self.body_props.alter_prop(op); + self + } + #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { self.regions @@ -559,6 +585,10 @@ impl Contextual for Context { &self.javascripts } + fn body_props(&self) -> &Props { + &self.body_props + } + // **< Contextual HELPERS >********************************************************************* fn remove_param(&mut self, key: &'static str) -> bool { diff --git a/src/core/component/definition.rs b/src/core/component/definition.rs index b7ceaa9a..f5af637e 100644 --- a/src/core/component/definition.rs +++ b/src/core/component/definition.rs @@ -34,11 +34,10 @@ pub trait ComponentRender { /// # Requisito: derivar `Clone` /// /// Todo tipo que implemente `Component` **debe** derivar también [`Clone`]. Aunque el compilador -/// no lo exige directamente —hacerlo rompería la seguridad de objeto de `dyn Component`—, -/// [`ComponentClone`] se implementa automáticamente mediante una *impl* blanket solo para los -/// tipos que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar -/// [`ComponentClone`] a mano, y el componente no podría registrarse en -/// [`InRegion`](crate::core::theme::InRegion). +/// no lo exige directamente (hacerlo rompería la seguridad de objeto de `dyn Component`), +/// [`ComponentClone`] se implementa automáticamente mediante una *impl* blanket solo para los tipos +/// que sean `Component + Clone + 'static`. Sin `Clone`, habría que implementar [`ComponentClone`] a +/// mano, y el componente no podría registrarse en [`InRegion`](crate::core::theme::InRegion). pub trait Component: AnyInfo + ComponentClone + ComponentRender + Send + Sync { /// Crea una nueva instancia del componente. /// diff --git a/src/core/component/error.rs b/src/core/component/error.rs index 86f9e4aa..9fa34d29 100644 --- a/src/core/component/error.rs +++ b/src/core/component/error.rs @@ -9,7 +9,7 @@ use crate::{AutoDefault, Getters}; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// # #[derive(Clone)] /// # struct MyComponent; diff --git a/src/core/component/message.rs b/src/core/component/message.rs index 9f6704a2..5d3125b4 100644 --- a/src/core/component/message.rs +++ b/src/core/component/message.rs @@ -25,7 +25,7 @@ pub enum MessageLevel { /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Mensaje informativo con clave traducible. /// let info = StatusMessage::new(MessageLevel::Info, L10n::l("saved-successfully")); diff --git a/src/core/extension/definition.rs b/src/core/extension/definition.rs index 984a5cc1..496001f9 100644 --- a/src/core/extension/definition.rs +++ b/src/core/extension/definition.rs @@ -10,7 +10,7 @@ use crate::web::Router; /// Este *trait* es fácil de implementar, basta con declarar una estructura sin campos para la /// extensión y sobrescribir los métodos que sean necesarios. Por ejemplo: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// pub struct MyExtension; /// @@ -48,7 +48,7 @@ pub trait Extension: AnyInfo + Send + Sync { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// pub struct MyTheme; /// @@ -107,7 +107,7 @@ pub trait Extension: AnyInfo + Send + Sync { /// /// ## Rutas HTTP básicas /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// # async fn list_posts() -> &'static str { "" } /// # async fn view_post() -> &'static str { "" } @@ -126,7 +126,7 @@ pub trait Extension: AnyInfo + Send + Sync { /// /// ## Rutas agrupadas bajo un prefijo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// # async fn dashboard() -> &'static str { "" } /// # async fn list_users() -> &'static str { "" } diff --git a/src/core/theme.rs b/src/core/theme.rs index 43649db1..9f358c41 100644 --- a/src/core/theme.rs +++ b/src/core/theme.rs @@ -238,8 +238,11 @@ impl Template for DefaultTemplate {} macro_rules! render_component { ($component:expr, { $($type:ty => |$var:ident| $body:expr),* $(,)? }) => { 'render_component: { + // Reborrow explícito como referencia compartida para que `downcast_ref` funcione + // correctamente con `&mut dyn Component` (limitación del compilador con trait objects). + let __c = &*($component); $( - if let Some($var) = ($component).downcast_ref::<$type>() { + if let Some($var) = __c.downcast_ref::<$type>() { break 'render_component Some($body); } )* diff --git a/src/core/theme/definition.rs b/src/core/theme/definition.rs index 0b036dd4..0e9e85e6 100644 --- a/src/core/theme/definition.rs +++ b/src/core/theme/definition.rs @@ -24,7 +24,7 @@ use crate::web::http::StatusCode; /// El único método **obligatorio** de `Extension` para un tema es [`theme()`](Extension::theme), /// que debe devolver una referencia al propio tema: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// pub struct MyTheme; /// diff --git a/src/core/theme/regions.rs b/src/core/theme/regions.rs index a10e3ecc..bbfce795 100644 --- a/src/core/theme/regions.rs +++ b/src/core/theme/regions.rs @@ -79,19 +79,19 @@ impl ChildrenInRegions { let mut result = Children::new(); - // 1. Prototipos globales comunes — clon fresco por cada página. + // 1. Prototipos globales comunes - clon fresco por cada página. if let Some(protos) = common.get(name) { for proto in protos { result.add(proto.as_child()); } } - // 2. Children propios de la página — se mueven (son por petición, no requieren clonado). + // 2. Children propios de la página - se mueven (son por petición, no requieren clonado). if let Some(page_children) = self.0.remove(name) { for child in page_children { result.add(child); } } - // 3. Prototipos del tema activo — clon fresco por cada página. + // 3. Prototipos del tema activo - clon fresco por cada página. if let Some(theme_map) = themed.get(&theme_ref.type_id()) { if let Some(protos) = theme_map.get(name) { for proto in protos { @@ -114,7 +114,7 @@ impl ChildrenInRegions { /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Banner global en la región de contenido. /// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" })); @@ -157,7 +157,7 @@ impl InRegion { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// // Banner global en la región por defecto. /// InRegion::Content.add(Html::with(|_| { diff --git a/src/global.rs b/src/global.rs index 2d7b1b60..17bbbe94 100644 --- a/src/global.rs +++ b/src/global.rs @@ -90,12 +90,11 @@ pub struct App { /// Sección **`[dev]`** de la configuración. Forma parte de [`Settings`]. #[derive(Debug, Deserialize)] pub struct Dev { - /// Directorio desde el que servir los archivos estáticos de PageTop. + /// Directorio raíz de `static/` para servir los archivos estáticos propios de PageTop. /// - /// Por defecto, los archivos se integran en el binario de la aplicación. Si aquí se indica una - /// ruta válida, ya sea absoluta o relativa al directorio del proyecto o del binario en - /// ejecución, se servirán desde el sistema de ficheros en su lugar. Esto es especialmente útil - /// en desarrollo, ya que evita recompilar el proyecto por cambios en estos archivos. + /// Si se indica una ruta válida, absoluta o relativa al directorio del proyecto o del binario + /// en ejecución, los archivos estáticos se servirán desde disco. Útil para poder modificar los + /// archivos estáticos mientras la aplicación está en ejecución, sin necesidad de recompilar. /// /// Si la cadena está vacía, se ignora este ajuste. pub pagetop_static_dir: String, diff --git a/src/html.rs b/src/html.rs index 6020627d..9f2daace 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,7 +1,7 @@ //! HTML en código. -mod maud; -pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, display, html, html_private}; +pub(crate) mod maud; +pub use maud::{DOCTYPE, Escaper, Markup, PreEscaped, Render, display, html, html_private}; mod route; pub use route::RoutePath; @@ -20,10 +20,10 @@ pub use logo::PageTopSvg; // **< HTML ATTRIBUTES >**************************************************************************** mod attr; -pub use attr::{Attr, AttrId, AttrName, AttrValue}; +pub use attr::{Attr, AttrName, AttrValue}; -mod classes; -pub use classes::{Classes, ClassesOp}; +mod props; +pub use props::{Props, PropsOp}; mod unit; pub use unit::UnitValue; diff --git a/src/html/assets/favicon.rs b/src/html/assets/favicon.rs index 9d0fb688..de5880b0 100644 --- a/src/html/assets/favicon.rs +++ b/src/html/assets/favicon.rs @@ -17,7 +17,7 @@ use crate::{AutoDefault, CowStr}; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// let favicon = Favicon::new() /// // Estándar de facto admitido por todos los navegadores. diff --git a/src/html/assets/javascript.rs b/src/html/assets/javascript.rs index 6af0fd55..03d3cff4 100644 --- a/src/html/assets/javascript.rs +++ b/src/html/assets/javascript.rs @@ -43,7 +43,7 @@ enum Source { /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Script externo con carga diferida, versión de caché y prioridad en el renderizado. /// let script = JavaScript::defer("/assets/js/app.js") diff --git a/src/html/assets/stylesheet.rs b/src/html/assets/stylesheet.rs index fb71fd44..d106ae8e 100644 --- a/src/html/assets/stylesheet.rs +++ b/src/html/assets/stylesheet.rs @@ -60,7 +60,7 @@ impl TargetMedia { /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Crea una hoja de estilos externa con control de versión y medio específico (`screen`). /// let stylesheet = StyleSheet::from("/assets/css/main.css") diff --git a/src/html/attr.rs b/src/html/attr.rs index 8f25a5eb..07fe52f5 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -8,7 +8,7 @@ use crate::{AutoDefault, builder_fn}; /// /// Este tipo **no impone ninguna normalización ni semántica concreta**; dichas reglas se definen en /// implementaciones concretas como `Attr` y `Attr`, o en tipos específicos como -/// [`AttrId`] y [`AttrName`]. +/// [`AttrName`]. #[derive(AutoDefault, Clone, Debug)] pub struct Attr(Option); @@ -128,73 +128,6 @@ impl Attr { } } -// **< AttrId >************************************************************************************* - -/// Identificador normalizado para el atributo `id` o similar de HTML. -/// -/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso: -/// -/// - Se eliminan los espacios al principio y al final. -/// - Se convierte a minúsculas. -/// - Se sustituyen los espacios (`' '`) intermedios por guiones bajos (`_`). -/// - Si el resultado es una cadena vacía, se guarda `None`. -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let id = AttrId::new(" main Section "); -/// assert_eq!(id.as_str(), Some("main_section")); -/// -/// let empty = AttrId::default(); -/// assert_eq!(empty.get(), None); -/// ``` -#[derive(AutoDefault, Clone, Debug)] -pub struct AttrId(Attr); - -impl AttrId { - /// Crea un nuevo `AttrId` normalizando el valor. - pub fn new(id: impl AsRef) -> Self { - Self::default().with_id(id) - } - - // **< AttrId BUILDER >************************************************************************* - - /// Establece un identificador nuevo normalizando el valor. - #[builder_fn] - pub fn with_id(mut self, id: impl AsRef) -> Self { - let id = id.as_ref().trim(); - if id.is_empty() { - self.0 = Attr::default(); - } else { - self.0 = Attr::some(id.to_ascii_lowercase().replace(' ', "_")); - } - self - } - - // **< AttrId GETTERS >************************************************************************* - - /// Devuelve el identificador normalizado, si existe. - pub fn get(&self) -> Option { - self.0.get() - } - - /// Devuelve el identificador normalizado (sin clonar), si existe. - pub fn as_str(&self) -> Option<&str> { - self.0.as_str() - } - - /// Devuelve el identificador normalizado (propiedad), si existe. - pub fn into_inner(self) -> Option { - self.0.into_inner() - } - - /// `true` si no hay valor. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - // **< AttrName >*********************************************************************************** /// Nombre normalizado para el atributo `name` o similar de HTML. diff --git a/src/html/classes.rs b/src/html/classes.rs deleted file mode 100644 index 903475ec..00000000 --- a/src/html/classes.rs +++ /dev/null @@ -1,205 +0,0 @@ -use crate::{AutoDefault, builder_fn, util}; - -use std::collections::HashSet; - -/// Operaciones disponibles sobre la lista de clases en [`Classes`]. -/// -/// Cada variante opera sobre **una o más clases** proporcionadas como una cadena separada por -/// espacios (p. ej. `"btn active"`), que se normalizan internamente a minúsculas en -/// [`Classes::with_classes()`]. -/// -/// # Orden de las clases y CSS -/// -/// El navegador aplica los estilos según la especificidad de los selectores y el orden en que las -/// reglas aparecen en la **hoja de estilos**, no por el orden de las clases en el atributo `class`. -/// Por tanto, `"btn active"` y `"active btn"` producen exactamente el mismo resultado visual. -/// -/// Las operaciones [`Add`](Self::Add) y [`Prepend`](Self::Prepend) permiten controlar ese orden -/// únicamente por legibilidad o por convención de proyecto, no porque afecte al comportamiento -/// del navegador. -/// -/// # Reemplazar una clase -/// -/// Para sustituir una clase por otra encadena [`Remove`](Self::Remove) y [`Add`](Self::Add): -/// ```rust -/// # use pagetop::prelude::*; -/// let c = Classes::new("btn btn-primary active") -/// .with_classes(ClassesOp::Remove, "btn-primary") -/// .with_classes(ClassesOp::Add, "btn-secondary"); -/// assert_eq!(c.get(), Some("btn active btn-secondary".to_string())); -/// ``` -#[derive(AutoDefault, Clone, Debug, PartialEq)] -pub enum ClassesOp { - /// Añade las clases que no existan al final. - #[default] - Add, - /// Añade las clases que no existan al principio. - Prepend, - /// Elimina las clases indicadas que existan. - Remove, - /// Alterna presencia/ausencia de una o más clases. - /// - /// Si en una misma llamada se repite una clase (p. ej. `"a a"`) que ya existe, el resultado - /// mantiene la pertenencia pero puede cambiar el orden (primero se elimina y luego se añade al - /// final). - Toggle, - /// Sustituye la lista completa por las clases indicadas. - Reset, -} - -/// Lista de clases CSS normalizadas para el atributo `class` de HTML. -/// -/// Permite construir y modificar dinámicamente con [`ClassesOp`] una lista de clases CSS -/// normalizadas. -/// -/// # Normalización -/// -/// - El orden de las clases no afecta al resultado en CSS; las operaciones de ordenación -/// ([`Add`](ClassesOp::Add), [`Prepend`](ClassesOp::Prepend)) son puramente estéticas. -/// - Solo se acepta una lista de clases con caracteres ASCII. -/// - Las clases se almacenan en minúsculas. -/// - No se permiten clases duplicadas tras la normalización (por ejemplo, `Btn` y `btn` se -/// consideran la misma clase). -/// - Las clases vacías se ignoran. -/// - Sin clases, [`get()`](Self::get) devuelve `None` (no `Some("")`). -/// -/// # Ejemplo -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let classes = Classes::new("Btn btn-primary") -/// .with_classes(ClassesOp::Add, "Active") -/// .with_classes(ClassesOp::Remove, "active") -/// .with_classes(ClassesOp::Add, "Disabled") -/// .with_classes(ClassesOp::Remove, "btn-primary"); -/// -/// assert_eq!(classes.get(), Some("btn disabled".to_string())); -/// assert!(classes.contains("disabled")); -/// ``` -#[derive(AutoDefault, Clone, Debug)] -pub struct Classes(Vec); - -impl Classes { - /// Crea una nueva lista de clases a partir de la clase o clases proporcionadas en `classes`. - pub fn new(classes: impl AsRef) -> Self { - Self::default().with_classes(ClassesOp::default(), classes) - } - - // **< Classes BUILDER >************************************************************************ - - /// Modifica la lista de clases según la operación indicada. - /// - /// Realiza la operación indicada en `op` para las clases proporcionadas en `classes` sobre la - /// lista de clases actual. - #[builder_fn] - pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - let Some(normalized) = - util::normalize_ascii_or_empty(classes.as_ref(), "Classes::with_classes") - else { - return self; - }; - match op { - ClassesOp::Add => { - self.add(normalized.as_ref().split_ascii_whitespace(), self.0.len()); - } - ClassesOp::Prepend => { - self.add(normalized.as_ref().split_ascii_whitespace(), 0); - } - ClassesOp::Remove => { - let mut classes_to_remove = normalized.as_ref().split_ascii_whitespace(); - - // 0 clases: no se hace nada. - let Some(first) = classes_to_remove.next() else { - return self; - }; - - // 1 clase: un único *retain*, sin reservas extra. - let Some(second) = classes_to_remove.next() else { - self.0.retain(|c| c != first); - return self; - }; - - // 2+ clases: HashSet y un único *retain*. - let mut to_remove: HashSet<&str> = HashSet::new(); - to_remove.insert(first); - to_remove.insert(second); - for class in classes_to_remove { - to_remove.insert(class); - } - self.0.retain(|c| !to_remove.contains(c.as_str())); - } - ClassesOp::Toggle => { - for class in normalized.as_ref().split_ascii_whitespace() { - if let Some(pos) = self.0.iter().position(|c| c == class) { - self.0.remove(pos); - } else { - self.0.push(class.to_string()); - } - } - } - ClassesOp::Reset => { - self.0.clear(); - self.add(normalized.as_ref().split_ascii_whitespace(), 0); - } - } - - self - } - - #[inline] - fn add<'a, I>(&mut self, classes: I, mut pos: usize) - where - I: IntoIterator, - { - for class in classes { - // Inserción segura descartando duplicados. - if !self.0.iter().any(|c| c == class) { - let class = class.to_string(); - if pos >= self.0.len() { - self.0.push(class); - } else { - self.0.insert(pos, class); - } - pos += 1; - } - } - } - - // **< Classes GETTERS >************************************************************************ - - /// Devuelve `true` si no hay clases. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Devuelve la cadena de clases, si existe. - pub fn get(&self) -> Option { - if self.0.is_empty() { - None - } else { - Some(self.0.join(" ")) - } - } - - /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes. - pub fn contains(&self, classes: impl AsRef) -> bool { - let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { - return false; - }; - normalized - .as_ref() - .split_ascii_whitespace() - .all(|class| self.0.iter().any(|c| c == class)) - } - - /// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente. - pub fn contains_any(&self, classes: impl AsRef) -> bool { - let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { - return false; - }; - normalized - .as_ref() - .split_ascii_whitespace() - .any(|class| self.0.iter().any(|c| c == class)) - } -} diff --git a/src/html/logo.rs b/src/html/logo.rs index 7746da7a..2e6711e5 100644 --- a/src/html/logo.rs +++ b/src/html/logo.rs @@ -7,7 +7,7 @@ use crate::locale::L10n; /// /// # Ejemplo /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// fn render_logo(cx: &mut Context) -> Markup { /// html! { diff --git a/src/html/maud.rs b/src/html/maud.rs index ab1236e4..cfc35105 100644 --- a/src/html/maud.rs +++ b/src/html/maud.rs @@ -252,7 +252,7 @@ impl Default for PreEscaped { /// /// A minimal web page: /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// /// let markup = html! { diff --git a/src/html/props.rs b/src/html/props.rs new file mode 100644 index 00000000..223924af --- /dev/null +++ b/src/html/props.rs @@ -0,0 +1,431 @@ +use crate::html::maud::{Escaper, Render}; +use crate::{AutoDefault, CowStr, builder_fn, util}; + +use std::fmt::Write; + +// **< PropsOp >************************************************************************************ + +/// Operaciones disponibles sobre atributos HTML y clases CSS en [`Props`]. +/// +/// Cada variante lleva los datos necesarios para ejecutarse. El método recomendado para usarlas es +/// recurrir a los constructores asociados como [`set()`](Self::set), [`set_id()`](Self::set_id), +/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc. +/// +/// Las variantes `*Id` operan sobre el atributo `id` del componente. Cuando se usa `"id"` como +/// nombre de atributo en `Set`, el valor se normaliza igual que en `SetId` o `EnsureId`. +/// +/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. Cuando +/// se usa `"class"` como nombre en `Set` o `Remove` la operación se aplica a la lista de clases +/// completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las nuevas clases +/// indicadas, y `Remove("class")` vacía la lista de clases. +#[derive(Clone, Debug, PartialEq)] +pub enum PropsOp { + /// Establece el identificador del elemento normalizando el valor: recorta espacios, convierte a + /// minúsculas y sustituye los espacios intermedios por `_`. Si el resultado es vacío, elimina + /// el identificador. + SetId(CowStr), + /// Establece el identificador del elemento si aún no hay ninguno definido, de modo que no + /// sobrescribe un valor asignado con anterioridad. Aplica la misma normalización que + /// [`SetId`](Self::SetId); si el resultado es vacío, la operación tampoco tiene efecto. + EnsureId(CowStr), + /// Añade el atributo o sustituye su valor si ya existe. Usar `"id"` como nombre aplica la misma + /// normalización que [`SetId`](Self::SetId). Usar `"class"` como nombre reemplaza la lista + /// completa de clases por las nuevas indicadas; la operación se ignora si el valor contiene + /// caracteres no ASCII. + Set(CowStr, CowStr), + /// Elimina el atributo indicado, incluido `"id"`. Si se usa `"class"` como nombre se vacía la + /// lista de clases. + Remove(CowStr), + /// Añade las clases que no existan al final de la lista. La operación se ignora si el valor + /// contiene caracteres no ASCII. + AddClasses(CowStr), + /// Añade las clases que no existan al principio de la lista. La operación se ignora si el valor + /// contiene caracteres no ASCII. + PrependClasses(CowStr), + /// Elimina las clases indicadas de la lista. La operación se ignora si el valor contiene + /// caracteres no ASCII. + RemoveClasses(CowStr), +} + +impl PropsOp { + /// Crea la variante [`SetId`](Self::SetId) con el identificador indicado. + pub fn set_id(id: impl Into) -> Self { + Self::SetId(id.into()) + } + + /// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado. + pub fn ensure_id(id: impl Into) -> Self { + Self::EnsureId(id.into()) + } + + /// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo. + pub fn set(name: impl Into, value: impl Into) -> Self { + Self::Set(name.into(), value.into()) + } + + /// Crea la variante [`Remove`](Self::Remove) para el atributo indicado. + pub fn remove(name: impl Into) -> Self { + Self::Remove(name.into()) + } + + /// Crea la variante [`AddClasses`](Self::AddClasses) con las clases indicadas. + pub fn add_classes(classes: impl Into) -> Self { + Self::AddClasses(classes.into()) + } + + /// Crea la variante [`PrependClasses`](Self::PrependClasses) con las clases indicadas. + pub fn prepend_classes(classes: impl Into) -> Self { + Self::PrependClasses(classes.into()) + } + + /// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con las clases indicadas. + pub fn remove_classes(classes: impl Into) -> Self { + Self::RemoveClasses(classes.into()) + } +} + +// **< Props >************************************************************************************** + +/// Colección de identificador, atributos HTML y clases CSS para aplicar en componentes. +/// +/// Al renderizar en `html!` emite primero `id` (si existe), luego `class` (si hay clases) y después +/// el resto de atributos. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// let props = Props::new("hx-get", "/api/items") +/// .with_prop(PropsOp::set("hx-target", "#lista")) +/// .with_prop(PropsOp::set("hx-swap", "outerHTML")); +/// +/// let markup = html! { +/// button (props) { "Cargar" } +/// }; +/// +/// assert_eq!( +/// markup.into_string(), +/// r##""## +/// ); +/// ``` +/// +/// # Identificadores +/// +/// [`SetId`](PropsOp::SetId) (usando [`PropsOp::set_id`]) normaliza el valor asignado al +/// identificador del componente: recorta espacios, convierte a minúsculas y sustituye los espacios +/// intermedios por `_`. +/// +/// ```rust +/// # use pagetop::prelude::*; +/// let props = Props::default().with_id("My Button"); +/// let markup = html! { button (props) { "OK" } }; +/// assert_eq!(markup.into_string(), r#""#); +/// ``` +/// +/// [`EnsureId`](PropsOp::EnsureId) (usando [`PropsOp::ensure_id`]) sólo asigna si no +/// hay identificador previo: +/// +/// ```rust +/// # use pagetop::prelude::*; +/// // Con `id` previo: `EnsureId` no tiene efecto. +/// let props = Props::default() +/// .with_id("explicit") +/// .with_prop(PropsOp::ensure_id("default")); +/// assert_eq!(props.get_id(), Some("explicit".to_string())); +/// +/// // Sin `id` previo: `EnsureId` asigna el valor. +/// let props = Props::default().with_prop(PropsOp::ensure_id("default")); +/// assert_eq!(props.get_id(), Some("default".to_string())); +/// ``` +/// +/// # Clases CSS +/// +/// ```rust +/// # use pagetop::prelude::*; +/// let props = Props::default() +/// .with_prop(PropsOp::add_classes("btn btn-primary")) +/// .with_prop(PropsOp::add_classes("active")); +/// +/// let markup = html! { button (props) { "OK" } }; +/// assert_eq!(markup.into_string(), r#""#); +/// ``` +/// +/// # Integración en componentes +/// +/// El patrón recomendado es añadir un campo `props: Props` con su método *builder* delegado: +/// +/// ```rust,no_run +/// # use pagetop::prelude::*; +/// #[derive(AutoDefault, Clone, Getters)] +/// pub struct MyButton { +/// label: L10n, +/// props: Props, +/// } +/// +/// impl Component for MyButton { +/// fn new() -> Self { Self::default() } +/// +/// fn prepare(&self, cx: &mut Context) -> Result { +/// Ok(html! { +/// button (self.props()) { +/// (self.label().using(cx)) +/// } +/// }) +/// } +/// } +/// +/// impl MyButton { +/// /// Modifica identificador, clases CSS o atributos HTML del elemento raíz. +/// #[builder_fn] +/// pub fn with_prop(mut self, op: PropsOp) -> Self { +/// self.props.alter_prop(op); +/// self +/// } +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug)] +pub struct Props { + id: Option, + attrs: Vec<(CowStr, CowStr)>, + classes: Vec, +} + +impl Props { + /// Crea una colección con un primer atributo ya establecido. + pub fn new(name: impl Into, value: impl Into) -> Self { + Self::default().with_prop(PropsOp::set(name, value)) + } + + /// Crea una colección con las clases CSS iniciales indicadas. + pub fn classes(classes: impl Into) -> Self { + Self::default().with_prop(PropsOp::add_classes(classes)) + } + + // **< Props BUILDER >************************************************************************** + + /// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`. + #[builder_fn] + pub fn with_id(mut self, id: impl Into) -> Self { + self.apply_id(id.into().as_ref()); + self + } + + /// Modifica el identificador, los atributos o las clases según la operación indicada. + /// + /// - [`SetId(value)`](PropsOp::SetId) establece el identificador normalizando el valor. + /// - [`EnsureId(value)`](PropsOp::EnsureId) establece el identificador (con la misma + /// normalización) sólo si no hay ninguno definido. + /// - [`Set(name, value)`](PropsOp::Set) añade el atributo o reemplaza su valor. + /// `Set("id", ...)` aplica la misma normalización que `SetId`. + /// `Set("class", ...)` reemplaza la lista de clases completa. + /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("id")` elimina el + /// identificador. `Remove("class")` vacía la lista de clases. + /// - [`AddClasses(clases)`](PropsOp::AddClasses) añade clases al final (sin duplicados). + /// - [`PrependClasses(clases)`](PropsOp::PrependClasses) añade clases al principio (sin + /// duplicados). + /// - [`RemoveClasses(clases)`](PropsOp::RemoveClasses) elimina las clases indicadas. + #[builder_fn] + pub fn with_prop(mut self, op: PropsOp) -> Self { + match op { + PropsOp::SetId(value) => { + self.apply_id(value.as_ref()); + } + PropsOp::EnsureId(value) => { + if self.id.is_none() { + self.apply_id(value.as_ref()); + } + } + PropsOp::Set(name, value) => { + if name.as_ref() == "id" { + self.apply_id(value.as_ref()); + } else if name.as_ref() == "class" { + if let Some(normalized) = + util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop") + { + self.classes.clear(); + self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0); + } + } else if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &name) { + self.attrs[pos].1 = value; + } else { + self.attrs.push((name, value)); + } + } + PropsOp::Remove(name) => { + if name.as_ref() == "id" { + self.id = None; + } else if name.as_ref() == "class" { + self.classes.clear(); + } else { + self.attrs.retain(|(k, _)| k != &name); + } + } + PropsOp::AddClasses(classes) => { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { + return self; + }; + let pos = self.classes.len(); + self.insert_classes(normalized.as_ref().split_ascii_whitespace(), pos); + } + PropsOp::PrependClasses(classes) => { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { + return self; + }; + self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0); + } + PropsOp::RemoveClasses(classes) => { + let Some(normalized) = + util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") + else { + return self; + }; + self.classes.retain(|c| { + !normalized + .as_ref() + .split_ascii_whitespace() + .any(|r| r == c.as_str()) + }); + } + } + self + } + + // **< Props GETTERS >************************************************************************** + + /// Devuelve el identificador normalizado del elemento, si existe. + #[inline] + pub fn get_id(&self) -> Option { + self.id.clone() + } + + /// Devuelve el valor del atributo indicado, si existe. + /// + /// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y + /// [`get_classes()`](Self::get_classes) respectivamente. + pub fn get_prop(&self, name: impl AsRef) -> Option { + match name.as_ref() { + "id" => self.id.clone(), + "class" => self.get_classes(), + name => self + .attrs + .iter() + .find(|(k, _)| k.as_ref() == name) + .map(|(_, v)| v.to_string()), + } + } + + /// Devuelve la lista de clases como cadena de texto, si hay clases definidas. + pub fn get_classes(&self) -> Option { + if self.classes.is_empty() { + None + } else { + Some(self.classes.join(" ")) + } + } + + /// Devuelve `true` si no hay ningún identificador definido. + #[inline] + pub fn is_id_empty(&self) -> bool { + self.id.is_none() + } + + /// Devuelve `true` si no hay ningún atributo extra definido, sin tener en cuenta el + /// identificador ni las clases. + #[inline] + pub fn is_attrs_empty(&self) -> bool { + self.attrs.is_empty() + } + + /// Devuelve `true` si no hay ninguna clase definida. + #[inline] + pub fn is_classes_empty(&self) -> bool { + self.classes.is_empty() + } + + /// Devuelve `true` si no hay ningún identificador, atributo ni clase definidos. + #[inline] + pub fn is_empty(&self) -> bool { + self.id.is_none() && self.attrs.is_empty() && self.classes.is_empty() + } + + /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes. + pub fn has_class(&self, classes: impl AsRef) -> bool { + let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { + return false; + }; + normalized + .as_ref() + .split_ascii_whitespace() + .all(|class| self.classes.iter().any(|c| c == class)) + } + + /// Devuelve `true` si la clase o **alguna** de las clases indicadas está presente. + pub fn has_any_class(&self, classes: impl AsRef) -> bool { + let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { + return false; + }; + normalized + .as_ref() + .split_ascii_whitespace() + .any(|class| self.classes.iter().any(|c| c == class)) + } + + // **< Props PRIVATE >************************************************************************** + + fn apply_id(&mut self, id: &str) { + let id = id.trim(); + self.id = if id.is_empty() { + None + } else { + Some(id.to_ascii_lowercase().replace(' ', "_")) + }; + } + + fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize) + where + I: IntoIterator, + { + for class in classes { + if !self.classes.iter().any(|c| c == class) { + let class = class.to_string(); + if pos >= self.classes.len() { + self.classes.push(class); + } else { + self.classes.insert(pos, class); + } + pos += 1; + } + } + } +} + +#[doc(hidden)] +impl Render for Props { + fn render_to(&self, w: &mut String) { + if let Some(id) = self.id.as_deref() { + w.push_str(" id=\""); + let _ = write!(Escaper::new(w), "{}", id); + w.push('"'); + } + if let Some((first, rest)) = self.classes.split_first() { + w.push_str(" class=\""); + let _ = write!(Escaper::new(w), "{}", first); + for class in rest { + w.push(' '); + let _ = write!(Escaper::new(w), "{}", class); + } + w.push('"'); + } + for (name, value) in &self.attrs { + w.push(' '); + let _ = write!(Escaper::new(w), "{}", name); + w.push_str("=\""); + let _ = write!(Escaper::new(w), "{}", value); + w.push('"'); + } + } +} diff --git a/src/html/unit.rs b/src/html/unit.rs index cb564c71..3df35612 100644 --- a/src/html/unit.rs +++ b/src/html/unit.rs @@ -263,7 +263,7 @@ impl FromStr for UnitValue { /// Deserializa desde una cadena usando la misma gramática que [`FromStr`]. /// /// # Ejemplo con `serde_json` -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// use serde::Deserialize; /// diff --git a/src/lib.rs b/src/lib.rs index 9e900335..40689318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ /*!
- +

PageTop

@@ -86,7 +86,7 @@ estructurar e inicializar la aplicación de forma modular. #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( - html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico" + html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico" )] // Alias para que las rutas absolutas `::pagetop::...` generadas por las macros funcionen en el @@ -102,7 +102,7 @@ use std::ops::Deref; /// /// Útil para versionar recursos estáticos de PageTop desde otros *crates*. Por ejemplo: /// -/// ```rust +/// ```rust,no_run /// use pagetop::prelude::*; /// /// pub struct MyTheme; @@ -138,14 +138,14 @@ pub use pagetop_statics::{StaticFile, resource}; pub use getter_methods::Getters; -/// Contenedor para un conjunto de recursos embebidos. +/// Contenedor para un paquete de recursos embebidos. #[derive(AutoDefault)] pub struct StaticResources { bundle: HashMap<&'static str, StaticFile>, } impl StaticResources { - /// Crea un contenedor para un conjunto de recursos generado por `build.rs` (consultar + /// Crea un contenedor para un paquete de recursos generado por `build.rs` (consultar /// [`pagetop_build`](https://docs.rs/pagetop-build)). pub fn new(bundle: HashMap<&'static str, StaticFile>) -> Self { Self { bundle } diff --git a/src/locale.rs b/src/locale.rs index a019d665..c9c6d7a8 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -75,7 +75,7 @@ //! Si los recursos se encuentran en el directorio por defecto `src/locale` del *crate*, sólo hay //! que declarar: //! -//! ```rust +//! ```rust,no_run //! # use pagetop::prelude::*; //! include_locales!(LOCALES_SAMPLE); //! ``` @@ -125,7 +125,7 @@ pub use l10n::L10n; /// /// Uso básico con el directorio por defecto `"src/locale"`: /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// include_locales!(LOCALES_SAMPLE); /// ``` diff --git a/src/locale/definition.rs b/src/locale/definition.rs index bffc805c..e9dfe9fd 100644 --- a/src/locale/definition.rs +++ b/src/locale/definition.rs @@ -54,7 +54,7 @@ pub trait LangId { /// resuelve un idioma soportado o porque se aplica el idioma por defecto o, en último término, el /// de respaldo (`"en-US"`): /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Idioma por defecto si no resuelve. /// let lang = Locale::resolve("it-IT"); diff --git a/src/locale/en-US/base.ftl b/src/locale/en-US/base.ftl index 76baa120..b244f9e2 100644 --- a/src/locale/en-US/base.ftl +++ b/src/locale/en-US/base.ftl @@ -1,3 +1,6 @@ +# Form components. +field_required = This field is required + # Intro component. intro_default_title = Hello, world! intro_default_slogan = Discover⚡{ $app } diff --git a/src/locale/es-ES/base.ftl b/src/locale/es-ES/base.ftl index 09867d13..50a459ec 100644 --- a/src/locale/es-ES/base.ftl +++ b/src/locale/es-ES/base.ftl @@ -1,3 +1,6 @@ +# Form components. +field_required = Este campo es obligatorio + # Intro component. intro_default_title = ¡Hola, mundo! intro_default_slogan = Descubre⚡{ $app } diff --git a/src/locale/l10n.rs b/src/locale/l10n.rs index e75e103d..66f20dc5 100644 --- a/src/locale/l10n.rs +++ b/src/locale/l10n.rs @@ -37,7 +37,7 @@ enum L10nOp { /// /// Los argumentos dinámicos se añaden con `with_arg()` o `with_args()`. /// -/// ```rust +/// ```rust,no_run /// # use pagetop::prelude::*; /// // Texto literal sin traducción. /// let raw = L10n::n("© 2025 PageTop").get(); @@ -128,7 +128,7 @@ impl L10n { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let text = L10n::l("greeting").with_arg("name", "Manuel").get(); /// ``` @@ -142,7 +142,7 @@ impl L10n { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// struct ResourceLang; /// @@ -180,7 +180,7 @@ impl L10n { /// /// # Ejemplo /// - /// ```rust + /// ```rust,no_run /// # use pagetop::prelude::*; /// let html = L10n::l("welcome.message").using(&Locale::resolve("es")); /// ``` diff --git a/src/prelude.rs b/src/prelude.rs index 5e6f7ec1..fbd84da3 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -36,7 +36,7 @@ pub use crate::locale::*; pub use crate::datetime::*; pub use crate::web; -pub use crate::web::{HttpRequest, Router}; +pub use crate::web::{HttpRequest, IntoResponse, Response, Router}; pub use crate::core::{AnyCast, AnyInfo, TypeInfo}; diff --git a/src/response/json.rs b/src/response/json.rs index 7ef4b402..64873839 100644 --- a/src/response/json.rs +++ b/src/response/json.rs @@ -5,7 +5,7 @@ //! Convierte automáticamente el cuerpo de una petición con `Content-Type: application/json` en un //! tipo Rust fuertemente tipado, validando el formato y deserializando con *serde*. //! -//! ```rust +//! ```rust,no_run //! # use pagetop::prelude::*; //! #[derive(serde::Deserialize)] //! struct NuevoUsuario { nombre: String, email: String } @@ -23,7 +23,7 @@ //! Serializa valores Rust a JSON y genera una respuesta HTTP con el encabezado apropiado //! `application/json; charset=utf-8`, todo con una llamada compacta. //! -//! ```rust +//! ```rust,no_run //! # use pagetop::prelude::*; //! #[derive(serde::Serialize)] //! struct Usuario { id: u32, nombre: String } diff --git a/src/response/page.rs b/src/response/page.rs index 2376fd39..129ccf03 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -20,8 +20,7 @@ use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{Attr, AttrId}; -use crate::html::{Classes, ClassesOp}; +use crate::html::{Attr, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::web::HttpRequest; @@ -90,8 +89,6 @@ pub struct Page { description : Attr, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, - body_id : AttrId, - body_classes: Classes, context : Context, } @@ -107,8 +104,6 @@ impl Page { description : Attr::::default(), metadata : Vec::default(), properties : Vec::default(), - body_id : AttrId::default(), - body_classes: Classes::default(), context : Context::new(Some(request)), } } @@ -143,20 +138,6 @@ impl Page { self } - /// Establece el atributo `id` del elemento ``. - #[builder_fn] - pub fn with_body_id(mut self, id: impl AsRef) -> Self { - self.body_id.alter_id(id); - self - } - - /// Modifica las clases CSS del elemento `` con una operación sobre [`Classes`]. - #[builder_fn] - pub fn with_body_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { - self.body_classes.alter_classes(op, classes); - self - } - // **< Page GETTERS >*************************************************************************** /// Devuelve el título traducido para el idioma de la página, si existe. @@ -179,16 +160,6 @@ impl Page { &self.properties } - /// Devuelve el identificador del elemento ``. - pub fn body_id(&self) -> &AttrId { - &self.body_id - } - - /// Devuelve las clases CSS del elemento ``. - pub fn body_classes(&self) -> &Classes { - &self.body_classes - } - /// Devuelve una referencia mutable al [`Context`] de la página. /// /// El [`Context`] actúa como intermediario para muchos métodos de `Page` (idioma, tema, @@ -262,7 +233,7 @@ impl Page { head { (head) } - body id=[self.body_id().get()] class=[self.body_classes().get()] { + body (self.body_props()) { (body) } } @@ -320,6 +291,12 @@ impl Contextual for Page { self } + #[builder_fn] + fn with_body_props(mut self, op: PropsOp) -> Self { + self.context.alter_body_props(op); + self + } + #[builder_fn] fn with_child(mut self, op: impl Into) -> Self { self.context @@ -363,6 +340,10 @@ impl Contextual for Page { self.context.javascripts() } + fn body_props(&self) -> &Props { + self.context.body_props() + } + // **< Contextual HELPERS >********************************************************************* fn remove_param(&mut self, key: &'static str) -> bool { diff --git a/static/css/basic.css b/static/css/basic.css deleted file mode 100644 index 9b23b272..00000000 --- a/static/css/basic.css +++ /dev/null @@ -1,43 +0,0 @@ -: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; - /* Colors */ - --val-color--bg: #fafafa; - --val-color--text: #212529; -} - -*, *::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; -} - -/* - * Region Footer - */ - -.region-footer { - padding: .75rem 0 3rem; - text-align: center; -} diff --git a/tests/component_children.rs b/tests/component_children.rs index b224f1b8..dc5eb5cc 100644 --- a/tests/component_children.rs +++ b/tests/component_children.rs @@ -1,13 +1,13 @@ use pagetop::prelude::*; -// **< TestComp — componente mínimo para los tests >************************************************ +// **< TestComp - componente mínimo para los tests >************************************************ // // Componente con id configurable y texto fijo de salida. El id permite probar las operaciones de // `Children` basadas en identificador (`InsertAfterId`, `RemoveById`, etc.). #[derive(AutoDefault, Clone)] struct TestComp { - id: AttrId, + props: Props, text: String, } @@ -17,7 +17,7 @@ impl Component for TestComp { } fn id(&self) -> Option { - self.id.get() + self.props.get_id() } fn prepare(&self, _cx: &mut Context) -> Result { @@ -29,7 +29,7 @@ impl TestComp { /// Crea un componente con id y texto de salida fijos. fn tagged(id: &str, text: &str) -> Self { let mut c = Self::default(); - c.id.alter_id(id); + c.props.alter_prop(PropsOp::set_id(id.to_string())); c.text = text.to_string(); c } @@ -303,7 +303,8 @@ async fn embed_get_allows_mutating_component() { let embed = Embed::with(TestComp::tagged("orig", "texto")); // El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`. if let Some(mut comp) = embed.get() { - comp.id.alter_id("modificado"); + comp.props + .alter_prop(PropsOp::set_id("modificado".to_string())); }; assert_eq!(embed.id(), Some("modificado".to_string())); } @@ -331,7 +332,8 @@ async fn embed_clone_is_deep() { let clone = original.clone(); // Mutar el clon no debe afectar al original. if let Some(mut comp) = clone.get() { - comp.id.alter_id("clone-id"); + comp.props + .alter_prop(PropsOp::set_id("clone-id".to_string())); } assert_eq!(original.id(), Some("orig".to_string())); assert_eq!(clone.id(), Some("clone-id".to_string())); diff --git a/tests/html_classes.rs b/tests/html_classes.rs index e2198335..6beeca44 100644 --- a/tests/html_classes.rs +++ b/tests/html_classes.rs @@ -1,7 +1,7 @@ use pagetop::prelude::*; -fn assert_classes(c: &Classes, expected: Option<&str>) { - let got = c.get(); +fn assert_classes(p: &Props, expected: Option<&str>) { + let got = p.get_classes(); assert_eq!( got.as_deref(), expected, @@ -15,176 +15,154 @@ fn assert_classes(c: &Classes, expected: Option<&str>) { #[pagetop::test] async fn classes_new_empty_and_whitespace_is_empty() { - assert_classes(&Classes::new(""), None); - assert_classes(&Classes::new(" "), None); - assert_classes(&Classes::new("\t\n\r "), None); + assert_classes(&Props::classes(""), None); + assert_classes(&Props::classes(" "), None); + assert_classes(&Props::classes("\t\n\r "), None); } #[pagetop::test] async fn classes_new_normalizes_and_dedups_and_preserves_first_occurrence_order() { - let c = Classes::new("Btn btn BTN btn-primary BTN-PRIMARY"); - assert_classes(&c, Some("btn btn-primary")); - assert!(c.contains("BTN")); - assert!(c.contains("btn-primary")); + let p = Props::classes("Btn btn BTN btn-primary BTN-PRIMARY"); + assert_classes(&p, Some("btn btn-primary")); + assert!(p.has_class("BTN")); + assert!(p.has_class("btn-primary")); } #[pagetop::test] async fn classes_get_returns_none_when_empty_some_when_not() { - assert_classes(&Classes::new(" "), None); - assert_classes(&Classes::new("a"), Some("a")); + assert_classes(&Props::classes(" "), None); + assert_classes(&Props::classes("a"), Some("a")); } // **< Basic operations (add/prepend/set) >********************************************************* #[pagetop::test] async fn classes_add_appends_unique_and_normalizes() { - let c = Classes::new("a b").with_classes(ClassesOp::Add, "C b D"); - assert_classes(&c, Some("a b c d")); + let p = Props::classes("a b").with_prop(PropsOp::add_classes("C b D")); + assert_classes(&p, Some("a b c d")); } #[pagetop::test] async fn classes_add_ignores_empty_input() { - let c = Classes::new("a b").with_classes(ClassesOp::Add, " \t"); - assert_classes(&c, Some("a b")); + let p = Props::classes("a b").with_prop(PropsOp::add_classes(" \t")); + assert_classes(&p, Some("a b")); } #[pagetop::test] async fn classes_add_same_tokens() { - let c = Classes::new("a b").with_classes(ClassesOp::Add, "A B a b"); - assert_classes(&c, Some("a b")); + let p = Props::classes("a b").with_prop(PropsOp::add_classes("A B a b")); + assert_classes(&p, Some("a b")); } #[pagetop::test] async fn classes_add_rejects_non_ascii_is_noop() { - let c = Classes::new("a b").with_classes(ClassesOp::Add, "c ñ d"); - assert_classes(&c, Some("a b")); + let p = Props::classes("a b").with_prop(PropsOp::add_classes("c ñ d")); + assert_classes(&p, Some("a b")); } #[pagetop::test] async fn classes_prepend_inserts_at_front_preserving_new_order() { - let c = Classes::new("c d").with_classes(ClassesOp::Prepend, "A b"); - assert_classes(&c, Some("a b c d")); + let p = Props::classes("c d").with_prop(PropsOp::prepend_classes("A b")); + assert_classes(&p, Some("a b c d")); } #[pagetop::test] async fn classes_prepend_inserts_new_tokens_skipping_duplicates() { - let c = Classes::new("b c").with_classes(ClassesOp::Prepend, "a b d"); - assert_classes(&c, Some("a d b c")); + let p = Props::classes("b c").with_prop(PropsOp::prepend_classes("a b d")); + assert_classes(&p, Some("a d b c")); } #[pagetop::test] async fn classes_prepend_ignores_empty_input() { - let c = Classes::new("a b").with_classes(ClassesOp::Prepend, ""); - assert_classes(&c, Some("a b")); + let p = Props::classes("a b").with_prop(PropsOp::prepend_classes("")); + assert_classes(&p, Some("a b")); } #[pagetop::test] async fn classes_reset_replaces_entire_list_and_dedups() { - let c = Classes::new("a b c").with_classes(ClassesOp::Reset, "X y y Z"); - assert_classes(&c, Some("x y z")); + let p = Props::classes("a b c").with_prop(PropsOp::set("class", "X y y Z")); + assert_classes(&p, Some("x y z")); } #[pagetop::test] async fn classes_reset_with_empty_input_clears() { - let base = Classes::new("a b"); - let c = base.with_classes(ClassesOp::Reset, " \n "); - assert_classes(&c, None); + let p = Props::classes("a b").with_prop(PropsOp::set("class", " \n ")); + assert_classes(&p, None); } -// **< Mutation operations (remove/toggle) >******************************************************** +#[pagetop::test] +async fn classes_reset_with_non_ascii_is_noop() { + let p = Props::classes("a b").with_prop(PropsOp::set("class", "ñ")); + assert_classes(&p, Some("a b")); +} + +// **< Mutation operations (remove) >*************************************************************** #[pagetop::test] async fn classes_remove_is_case_insensitive() { - let c = Classes::new("a b c d").with_classes(ClassesOp::Remove, "B D"); - assert_classes(&c, Some("a c")); + let p = Props::classes("a b c d").with_prop(PropsOp::remove_classes("B D")); + assert_classes(&p, Some("a c")); } #[pagetop::test] async fn classes_remove_non_existing_is_noop() { - let c = Classes::new("a b c").with_classes(ClassesOp::Remove, "x y z"); - assert_classes(&c, Some("a b c")); + let p = Props::classes("a b c").with_prop(PropsOp::remove_classes("x y z")); + assert_classes(&p, Some("a b c")); } #[pagetop::test] async fn classes_remove_with_extra_whitespace() { - let c = Classes::new("a b c d").with_classes(ClassesOp::Remove, " b\t\t \n d "); - assert_classes(&c, Some("a c")); -} - -#[pagetop::test] -async fn classes_toggle_removes_if_present_case_insensitive() { - let c = Classes::new("a b c").with_classes(ClassesOp::Toggle, "B"); - assert_classes(&c, Some("a c")); -} - -#[pagetop::test] -async fn classes_toggle_adds_if_missing_and_normalizes() { - let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "C"); - assert_classes(&c, Some("a b c")); -} - -#[pagetop::test] -async fn classes_toggle_multiple_tokens_is_sequential_and_order_dependent() { - let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "C B A"); - assert_classes(&c, Some("c")); -} - -#[pagetop::test] -async fn classes_toggle_duplicate_tokens_are_applied_sequentially() { - let c = Classes::new("b").with_classes(ClassesOp::Toggle, "a a"); - assert_classes(&c, Some("b")); - - let c = Classes::new("a b").with_classes(ClassesOp::Toggle, "a a"); - assert_classes(&c, Some("b a")); + let p = Props::classes("a b c d").with_prop(PropsOp::remove_classes(" b\t\t \n d ")); + assert_classes(&p, Some("a c")); } // **< Queries (contains) >************************************************************************* #[pagetop::test] async fn classes_contains_single() { - let c = Classes::new("btn btn-primary"); - assert!(c.contains("btn")); - assert!(c.contains("BTN")); - assert!(!c.contains("missing")); + let p = Props::classes("btn btn-primary"); + assert!(p.has_class("btn")); + assert!(p.has_class("BTN")); + assert!(!p.has_class("missing")); } #[pagetop::test] async fn classes_contains_all_and_any() { - let c = Classes::new("btn btn-primary active"); + let p = Props::classes("btn btn-primary active"); - assert!(c.contains("btn active")); - assert!(c.contains("BTN BTN-PRIMARY")); - assert!(!c.contains("btn missing")); + assert!(p.has_class("btn active")); + assert!(p.has_class("BTN BTN-PRIMARY")); + assert!(!p.has_class("btn missing")); - assert!(c.contains_any("missing active")); - assert!(c.contains_any("BTN-PRIMARY missing")); - assert!(!c.contains_any("missing other")); + assert!(p.has_any_class("missing active")); + assert!(p.has_any_class("BTN-PRIMARY missing")); + assert!(!p.has_any_class("missing other")); } #[pagetop::test] async fn classes_contains_empty_and_whitespace_is_false() { - let c = Classes::new("a b"); - assert!(!c.contains("")); - assert!(!c.contains(" \t")); - assert!(!c.contains_any("")); - assert!(!c.contains_any(" \n ")); + let p = Props::classes("a b"); + assert!(!p.has_class("")); + assert!(!p.has_class(" \t")); + assert!(!p.has_any_class("")); + assert!(!p.has_any_class(" \n ")); } #[pagetop::test] async fn classes_contains_non_ascii_is_false() { - let c = Classes::new("a b"); - assert!(!c.contains("ñ")); - assert!(!c.contains_any("a ñ")); + let p = Props::classes("a b"); + assert!(!p.has_class("ñ")); + assert!(!p.has_any_class("a ñ")); } // **< Properties / regression (combined sequences, ordering) >************************************* #[pagetop::test] async fn classes_order_is_stable_for_existing_items() { - let c = Classes::new("a b c") - .with_classes(ClassesOp::Add, "d") // a b c d - .with_classes(ClassesOp::Prepend, "x") // x a b c d - .with_classes(ClassesOp::Remove, "b") // x a c d - .with_classes(ClassesOp::Add, "b"); // x a c d b - assert_classes(&c, Some("x a c d b")); + let p = Props::classes("a b c") + .with_prop(PropsOp::add_classes("d")) // a b c d + .with_prop(PropsOp::prepend_classes("x")) // x a b c d + .with_prop(PropsOp::remove_classes("b")) // x a c d + .with_prop(PropsOp::add_classes("b")); // x a c d b + assert_classes(&p, Some("x a c d b")); } diff --git a/tests/html_props.rs b/tests/html_props.rs new file mode 100644 index 00000000..3db26746 --- /dev/null +++ b/tests/html_props.rs @@ -0,0 +1,363 @@ +use pagetop::prelude::*; + +// **< Construction & invariants >****************************************************************** + +#[pagetop::test] +async fn props_default_renders_nothing() { + assert_eq!( + html! { span (Props::default()) {} }.into_string(), + "" + ); +} + +#[pagetop::test] +async fn props_new_creates_first_attr() { + let p = Props::new("hx-get", "/api"); + assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); +} + +#[pagetop::test] +async fn props_get_missing_key_returns_none() { + let p = Props::new("hx-get", "/api"); + assert_eq!(p.get_prop("hx-post"), None); + assert_eq!(p.get_prop(""), None); +} + +// **< Props::classes >***************************************************************************** + +#[pagetop::test] +async fn props_classes_renders_class_attribute() { + let p = Props::classes("btn btn-primary"); + assert_eq!( + html! { button (p) { "OK" } }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_classes_empty_input_renders_no_class_attribute() { + let p = Props::classes(" "); + assert_eq!( + html! { button (p) { "OK" } }.into_string(), + "" + ); +} + +#[pagetop::test] +async fn props_classes_can_be_extended_with_with_prop() { + let p = Props::classes("btn").with_prop(PropsOp::add_classes("active")); + assert_eq!( + html! { button (p) { "OK" } }.into_string(), + r#""# + ); +} + +// **< PropsOp::set >******************************************************************************* + +#[pagetop::test] +async fn props_set_adds_new_attrs() { + let p = Props::default() + .with_prop(PropsOp::set("hx-get", "/api")) + .with_prop(PropsOp::set("hx-swap", "outerHTML")); + assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); + assert_eq!(p.get_prop("hx-swap"), Some("outerHTML".to_string())); +} + +#[pagetop::test] +async fn props_set_replaces_existing_value() { + let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new")); + assert_eq!(p.get_prop("hx-get"), Some("/new".to_string())); +} + +#[pagetop::test] +async fn props_set_does_not_create_duplicate_key() { + // Reasignar la misma clave debe reemplazar el valor, no añadir una entrada duplicada. + let p = Props::new("key", "v1").with_prop(PropsOp::set("key", "v2")); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_set_preserves_insertion_order() { + let p = Props::new("a", "1") + .with_prop(PropsOp::set("b", "2")) + .with_prop(PropsOp::set("c", "3")); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +// **< PropsOp::remove >**************************************************************************** + +#[pagetop::test] +async fn props_remove_existing_attr() { + let p = Props::new("a", "1") + .with_prop(PropsOp::set("b", "2")) + .with_prop(PropsOp::remove("a")); + assert_eq!(p.get_prop("a"), None); + assert_eq!(p.get_prop("b"), Some("2".to_string())); +} + +#[pagetop::test] +async fn props_remove_nonexistent_key_is_noop() { + let p = Props::new("a", "1").with_prop(PropsOp::remove("missing")); + assert_eq!(p.get_prop("a"), Some("1".to_string())); + assert_eq!(p.get_prop("missing"), None); +} + +#[pagetop::test] +async fn props_renders_nothing_after_removing_last_attr() { + let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); + assert_eq!(html! { span (p) {} }.into_string(), ""); +} + +// **< HTML Escaped >******************************************************************************* + +#[pagetop::test] +async fn props_escapes_ampersand_and_angle_brackets_in_value() { + let p = Props::new("data-info", "a&bd"); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_escapes_double_quotes_in_value() { + let p = Props::new("data-label", r#"say "hello""#); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +// **< Integration with html! >********************************************************************* + +#[pagetop::test] +async fn props_empty_in_html_macro_produces_no_attributes() { + // Una Props vacía no debe emitir ni siquiera un espacio en blanco extra. + let p = Props::default(); + assert_eq!( + html! { button (p) { "x" } }.into_string(), + "" + ); +} + +#[pagetop::test] +async fn props_single_attr_in_html_macro() { + let p = Props::new("hx-get", "/api"); + assert_eq!( + html! { button (p) { "Load" } }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_multiple_attrs_preserve_order_in_html_macro() { + let p = Props::new("hx-get", "/api") + .with_prop(PropsOp::set("hx-target", "#result")) + .with_prop(PropsOp::set("hx-swap", "outerHTML")); + assert_eq!( + html! { button (p) {} }.into_string(), + r##""## + ); +} + +#[pagetop::test] +async fn props_alongside_class_and_id_in_html_macro() { + // El splice siempre se emite después de class e id, independientemente del orden escrito. + let p = Props::new("hx-get", "/api"); + assert_eq!( + html! { button #mybtn .btn (p) { "Go" } }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_alongside_named_attr_renders_after_it() { + let p = Props::new("hx-get", "/api"); + assert_eq!( + html! { button type="button" (p) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_multiple_splices_in_same_element() { + let p1 = Props::new("hx-get", "/api"); + let p2 = Props::new("hx-swap", "outerHTML"); + assert_eq!( + html! { button (p1) (p2) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_inline_construction_in_html_macro() { + assert_eq!( + html! { button (Props::new("hx-get", "/api")) { "Go" } }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_conditional_expression_in_html_macro() { + for (active, expected) in [ + (true, r#""#), + (false, ""), + ] { + let markup = html! { + button (if active { Props::new("hx-get", "/api") } else { Props::default() }) { "x" } + }; + assert_eq!(markup.into_string(), expected); + } +} + +#[pagetop::test] +async fn props_splice_empty_string_emits_nothing() { + // Un splice vacío no emite ningún atributo ni espacio extra. + assert_eq!(html! { span ("") { "x" } }.into_string(), "x"); +} + +// **< is_attrs_empty / is_classes_empty / is_empty >*********************************************** + +#[pagetop::test] +async fn props_is_attrs_empty_on_default() { + assert!(Props::default().is_attrs_empty()); +} + +#[pagetop::test] +async fn props_is_attrs_empty_false_after_set() { + assert!(!Props::new("hx-get", "/api").is_attrs_empty()); +} + +#[pagetop::test] +async fn props_is_attrs_empty_true_after_removing_last_attr() { + let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); + assert!(p.is_attrs_empty()); +} + +#[pagetop::test] +async fn props_is_empty_on_default() { + assert!(Props::default().is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_id() { + assert!(!Props::default().with_id("main").is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_attr() { + assert!(!Props::new("hx-get", "/api").is_empty()); +} + +#[pagetop::test] +async fn props_is_empty_false_with_class() { + assert!(!Props::classes("btn").is_empty()); +} + +#[pagetop::test] +async fn props_is_classes_empty_on_default() { + assert!(Props::default().is_classes_empty()); +} + +#[pagetop::test] +async fn props_is_classes_empty_false_after_add_classes() { + assert!(!Props::classes("btn").is_classes_empty()); +} + +#[pagetop::test] +async fn props_is_classes_empty_true_after_remove_class() { + let p = Props::classes("btn").with_prop(PropsOp::remove("class")); + assert!(p.is_classes_empty()); +} + +// **< get_prop("id") / get_prop("class") >********************************************************* + +#[pagetop::test] +async fn get_prop_id_returns_none_by_default() { + assert_eq!(Props::default().get_prop("id"), None); +} + +#[pagetop::test] +async fn get_prop_id_returns_normalized_value() { + let p = Props::default().with_id("My Button"); + assert_eq!(p.get_prop("id"), Some("my_button".to_string())); +} + +#[pagetop::test] +async fn get_prop_id_matches_get_id() { + let p = Props::default().with_id("Header"); + assert_eq!(p.get_prop("id"), p.get_id()); +} + +#[pagetop::test] +async fn get_prop_class_returns_none_by_default() { + assert_eq!(Props::default().get_prop("class"), None); +} + +#[pagetop::test] +async fn get_prop_class_returns_joined_classes() { + let p = Props::classes("btn btn-primary").with_prop(PropsOp::add_classes("active")); + assert_eq!( + p.get_prop("class"), + Some("btn btn-primary active".to_string()) + ); +} + +#[pagetop::test] +async fn get_prop_class_matches_get_classes() { + let p = Props::classes("btn active"); + assert_eq!(p.get_prop("class"), p.get_classes()); +} + +// **< Regression & edge cases >******************************************************************** + +#[pagetop::test] +async fn props_hx_target_value_with_hash_renders_correctly() { + // Regresión: r#"..."# se cerraba prematuramente al encontrar `"#lista"`. + let p = Props::new("hx-target", "#list"); + assert_eq!( + html! { button (p) {} }.into_string(), + r##""## + ); +} + +#[pagetop::test] +async fn props_with_empty_value_renders_attr_with_empty_value() { + let p = Props::new("data-expanded", ""); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_chained_set_and_remove_yields_expected_state() { + let p = Props::new("a", "1") + .with_prop(PropsOp::set("b", "2")) + .with_prop(PropsOp::set("c", "3")) + .with_prop(PropsOp::remove("b")) + .with_prop(PropsOp::set("a", "updated")); + assert_eq!(p.get_prop("a"), Some("updated".to_string())); + assert_eq!(p.get_prop("b"), None); + assert_eq!(p.get_prop("c"), Some("3".to_string())); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} + +#[pagetop::test] +async fn props_with_empty_attr_name_renders_without_validation() { + // Comportamiento documentado: los nombres no se validan; el HTML resultante no es estándar. + let p = Props::new("", "val"); + assert_eq!( + html! { span (p) {} }.into_string(), + r#""# + ); +} diff --git a/tools/changelog.sh b/tools/changelog.sh index fc2a8a0c..81c85747 100755 --- a/tools/changelog.sh +++ b/tools/changelog.sh @@ -65,6 +65,7 @@ case "$CRATE" in # Extensions --exclude-path "extensions/pagetop-aliner/**/*" --exclude-path "extensions/pagetop-bootsier/**/*" + --exclude-path "extensions/pagetop-htmx/**/*" --exclude-path "extensions/pagetop-seaorm/**/*" ) ;; @@ -76,6 +77,10 @@ case "$CRATE" in CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md" PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*") ;; + pagetop-htmx) + CHANGELOG_FILE="extensions/pagetop-htmx/CHANGELOG.md" + PATH_FLAGS=(--include-path "extensions/pagetop-htmx/**/*") + ;; pagetop-seaorm) CHANGELOG_FILE="extensions/pagetop-seaorm/CHANGELOG.md" PATH_FLAGS=(--include-path "extensions/pagetop-seaorm/**/*")