From f42a5f05b2598781a4dcfc7b77108e1f8f7cc6bd Mon Sep 17 00:00:00 2001 From: Enciphered Date: Sat, 30 May 2026 01:09:14 +0500 Subject: [PATCH] feat: initialize template shell and basic components --- .dockerignore | 5 + .env.example | 11 + .gitignore | 3 + Cargo.lock | 3548 ++++++++++++++++++++++ Cargo.toml | 26 + Dockerfile | 69 + README.md | 92 + package-lock.json | 1037 +++++++ package.json | 17 + src/auth/extractors.rs | 87 + src/auth/handlers.rs | 207 ++ src/auth/mod.rs | 23 + src/auth/models.rs | 21 + src/auth/repository.rs | 52 + src/common/config.rs | 42 + src/common/database.rs | 44 + src/common/errors.rs | 94 + src/common/mod.rs | 3 + src/components/handlers.rs | 209 ++ src/components/mod.rs | 30 + src/developers/handlers.rs | 207 ++ src/developers/mod.rs | 23 + src/developers/models.rs | 16 + src/developers/repository.rs | 123 + src/input.css | 185 ++ src/main.rs | 107 + src/main_view/mod.rs | 79 + src/tasks/handlers.rs | 150 + src/tasks/mod.rs | 24 + src/tasks/models.rs | 18 + src/tasks/repository.rs | 99 + static/js/combobox.js | 201 ++ static/js/components.js | 669 ++++ static/tailwind.css | 2799 +++++++++++++++++ templates/auth/login.html | 48 + templates/auth/register.html | 57 + templates/base.html | 97 + templates/components/buttons.html | 135 + templates/components/combobox.html | 216 ++ templates/components/date_time.html | 392 +++ templates/components/feedback.html | 130 + templates/components/index.html | 133 + templates/components/inputs.html | 221 ++ templates/components/modals.html | 169 ++ templates/components/scrollbars.html | 105 + templates/components/sheets.html | 180 ++ templates/components/sidebar.html | 27 + templates/components/tabs_accordion.html | 238 ++ templates/components/toggles.html | 124 + templates/components/visuals.html | 104 + templates/developers/edit.html | 42 + templates/developers/list.html | 110 + templates/developers/search_results.html | 15 + templates/main_view/index.html | 75 + templates/tasks/dashboard.html | 169 ++ 55 files changed, 13107 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/auth/extractors.rs create mode 100644 src/auth/handlers.rs create mode 100644 src/auth/mod.rs create mode 100644 src/auth/models.rs create mode 100644 src/auth/repository.rs create mode 100644 src/common/config.rs create mode 100644 src/common/database.rs create mode 100644 src/common/errors.rs create mode 100644 src/common/mod.rs create mode 100644 src/components/handlers.rs create mode 100644 src/components/mod.rs create mode 100644 src/developers/handlers.rs create mode 100644 src/developers/mod.rs create mode 100644 src/developers/models.rs create mode 100644 src/developers/repository.rs create mode 100644 src/input.css create mode 100644 src/main.rs create mode 100644 src/main_view/mod.rs create mode 100644 src/tasks/handlers.rs create mode 100644 src/tasks/mod.rs create mode 100644 src/tasks/models.rs create mode 100644 src/tasks/repository.rs create mode 100644 static/js/combobox.js create mode 100644 static/js/components.js create mode 100644 static/tailwind.css create mode 100644 templates/auth/login.html create mode 100644 templates/auth/register.html create mode 100644 templates/base.html create mode 100644 templates/components/buttons.html create mode 100644 templates/components/combobox.html create mode 100644 templates/components/date_time.html create mode 100644 templates/components/feedback.html create mode 100644 templates/components/index.html create mode 100644 templates/components/inputs.html create mode 100644 templates/components/modals.html create mode 100644 templates/components/scrollbars.html create mode 100644 templates/components/sheets.html create mode 100644 templates/components/sidebar.html create mode 100644 templates/components/tabs_accordion.html create mode 100644 templates/components/toggles.html create mode 100644 templates/components/visuals.html create mode 100644 templates/developers/edit.html create mode 100644 templates/developers/list.html create mode 100644 templates/developers/search_results.html create mode 100644 templates/main_view/index.html create mode 100644 templates/tasks/dashboard.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9bf44e3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +target +.git +.antigravitycli +cookie.txt +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d8389cf --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# MongoDB Connection Options +DATABASE_URL=mongodb://localhost:27017 +DATABASE_NAME=stick_db + +# Cryptography Settings +# Ensure this secret is a strong, random character sequence in production +JWT_SECRET=super_secret_template_signing_key_that_is_at_least_32_characters_long + +# Web Server Options +HOST=127.0.0.1 +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b99a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +node_modules/ +.antigravitycli/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b127d5c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3548 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "askama" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" +dependencies = [ + "askama_macros", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" +dependencies = [ + "askama_parser", + "basic-toml", + "glob", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "askama_macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" +dependencies = [ + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" +dependencies = [ + "rustc-hash", + "serde", + "serde_derive", + "unicode-ident", + "winnow", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bcrypt" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ae5479c93d3720e4c1dbd6b945b97457c50cb672781104768190371df1a905" +dependencies = [ + "base64", + "blowfish", + "getrandom 0.4.2", + "subtle", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.14.0", + "js-sys", + "once_cell", + "rand 0.9.4", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bson" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f109694c4f45353972af96bf97d8a057f82e2d6e496457f4d135b9867a518c" +dependencies = [ + "ahash", + "base64", + "bitvec", + "chrono", + "getrandom 0.3.4", + "hex", + "indexmap 2.14.0", + "js-sys", + "rand 0.9.4", + "serde", + "serde_bytes", + "serde_with", + "simdutf8", + "thiserror", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "smallvec", + "system-configuration", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" +dependencies = [ + "bson 2.15.0", + "bson 3.1.0", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.5+1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" + +[[package]] +name = "mongodb" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ba0cd571553d1f6936c6f180964776ece6ab7507dc8765f8a9c9c49d8cd00" +dependencies = [ + "base64", + "bitflags", + "bson 2.15.0", + "bson 3.1.0", + "derive-where", + "derive_more", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-net", + "hickory-proto", + "hickory-resolver", + "hmac", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand 0.9.4", + "rustc_version_runtime", + "rustls", + "serde", + "serde_bytes", + "serde_with", + "sha1", + "sha2", + "socket2", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ceb1a9a1018e470077ec94cf3a8c2d0e6da542b2c05ea95a59a0a627147375" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stick" +version = "0.1.0" +dependencies = [ + "askama", + "async-trait", + "axum", + "axum-extra", + "bcrypt", + "bson 3.1.0", + "chrono", + "dotenvy", + "futures", + "jsonwebtoken", + "mongodb", + "serde", + "serde_json", + "serde_with", + "thiserror", + "time", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..04a3ed2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "stick" +version = "0.1.0" +edition = "2024" + +[dependencies] +askama = "0.16.0" +async-trait = "0.1.89" +axum = { version = "0.8.9", features = ["macros"] } +axum-extra = { version = "0.12.6", features = ["cookie"] } +bcrypt = "0.19.1" +bson = { version = "3.1.0", features = ["chrono-0_4", "serde", "serde_with-3"] } +chrono = { version = "0.4.44", features = ["serde"] } +dotenvy = "0.15.7" +futures = "0.3.32" +jsonwebtoken = { version = "10.4.0", features = ["rust_crypto"] } +mongodb = { version = "3.7.0", features = ["bson-3"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +serde_with = "3.20.0" +thiserror = "2.0.18" +time = "0.3.47" +tokio = { version = "1.52.3", features = ["full"] } +tower-http = { version = "0.6.11", features = ["trace", "cors"] } +tracing = "0.1.44" +tracing-subscriber = "0.3.23" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bfb1ff2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ +# Stage 1: Build Tailwind CSS +FROM node:20-slim AS tailwind-builder +WORKDIR /app + +# Copy dependency manifests and install dependencies +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy source, templates, and static directories for Tailwind compile scan +COPY src ./src +COPY templates ./templates +COPY static ./static + +# Run Tailwind compilation +RUN npx tailwindcss -i src/input.css -o static/tailwind.css + +# Stage 2: Build Rust application +FROM rust:1.95-slim AS builder +WORKDIR /app + +# Create a dummy project to cache dependencies +RUN cargo new --bin stick +WORKDIR /app/stick + +# Copy dependency manifests +COPY Cargo.toml Cargo.lock ./ + +# Build dependencies (cached as a layer unless Cargo.toml/Cargo.lock changes) +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/stick/target \ + cargo build + +# Copy tailwind.css compiled in the first stage +COPY --from=tailwind-builder /app/static/tailwind.css ./static/tailwind.css + +# Copy the actual source code, templates, and rest of static files +COPY src ./src +COPY templates ./templates +COPY static ./static +# Ensure the compiled tailwind.css is definitely the one used +COPY --from=tailwind-builder /app/static/tailwind.css ./static/tailwind.css + +# Build the real binary +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/stick/target \ + touch src/main.rs && \ + cargo build && \ + cp target/debug/stick /app/stick-bin + +# Stage 3: Runtime stage +FROM debian:bookworm-slim +WORKDIR /app + +# Install runtime dependencies if needed +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +# Copy the binary and templates from the build stage +COPY --from=builder /app/stick-bin /app/stick +COPY --from=builder /app/stick/templates /app/templates + +# Expose port 3007 +EXPOSE 3007 + +# Set default HOST and PORT +ENV HOST=0.0.0.0 +ENV PORT=3007 + +# Run the app +CMD ["/app/stick"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0c8a1f --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Stick: Use-Case Oriented Axum + Askama + MongoDB Template + +A production-ready Rust web application template organized by vertical features (use-cases) rather than horizontal technical layers. + +--- + +## 🛠️ Technology Stack +* **Web Framework**: [Axum](https://github.com/tokio-rs/axum) (v0.8) - Native RPITIT (Return-Position Impl Trait in Traits) extractors. +* **Template Engine**: [Askama](https://github.com/djc/askama) (v0.16) - Type-safe, compile-time HTML templates. +* **Styling**: [Tailwind CSS](https://tailwindcss.com/) - Modern utility-first styling. +* **Database**: [MongoDB Rust Driver](https://github.com/mongodb/mongo-rust-driver) (v3.7) - Configured with BSON v3. +* **Authentication/Authorization**: [jsonwebtoken](https://github.com/Keats/jsonwebtoken) (v10) with the `rust_crypto` backend, stored in secure `HttpOnly` session cookies. +* **Password Hashing**: [bcrypt](https://github.com/Keats/rust-bcrypt) for secure password storage. + +--- + +## 📁 Use-Case Centered Project Layout +Unlike traditional MVC setups, files are grouped by their business domain. A single use-case directory contains its models, database repositories, route handlers, local extractors, and templates. + +```text +stick/ +├── Cargo.toml +├── .env.example +├── templates/ # Raw HTML template layout files +│ ├── base.html # Global layout wrapper +│ ├── auth/ # Auth views +│ │ ├── login.html +│ │ └── register.html +│ ├── tasks/ # Task manager views +│ │ └── dashboard.html +│ └── main_view/ # Main views +│ └── index.html +└── src/ + ├── main.rs # Server composition, shared state, and route merging + ├── common/ # Shared features (errors, database, settings) + │ ├── config.rs + │ ├── database.rs + │ └── errors.rs + ├── auth/ # Authentication & User Management Use-Case + │ ├── extractors.rs # Session context extractors + │ ├── handlers.rs # User interaction handlers + │ ├── models.rs # User database schemas + │ └── repository.rs # User database actions + ├── tasks/ # Tasks & Dashboard Management Use-Case + │ ├── handlers.rs # Task CRUD handlers + │ ├── models.rs # Task database schemas + │ └── repository.rs # Task database actions + └── main_view/ # Static Navigation & Branding Use-Case + └── mod.rs # Serves index & handles public routes +``` + +--- + +## 🚀 Setup & Execution + +### 1. Prerequisites +* [Rust](https://www.rust-lang.org/tools/install) (v1.75+ required for native async traits) +* [MongoDB](https://www.mongodb.com/) running locally (port `27017`) + +### 2. Configuration +Copy the configuration example file and customize your settings: +```bash +cp .env.example .env +``` + +### 3. Run the Server +Start the development server: +```bash +cargo run +``` +The server will start listening at `http://127.0.0.1:3000`. + +--- + +## 💡 Designing Custom Use-Cases +When adding a new feature (e.g., `projects`): + +1. Create `src/projects/` containing: + * `models.rs` (BSON schemas) + * `repository.rs` (Database access) + * `handlers.rs` (Endpoints) + * `mod.rs` (Usecase module entrypoint exposing a `pub fn router() -> Router`) +2. Add its view templates under `templates/projects/`. +3. Expose the repository and compile constraints in `src/main.rs`. +4. Merge the usecase router inside the main router builder: + ```rust + let app = Router::new() + .merge(main_view::router()) + .merge(auth::router()) + .merge(projects::router()) // Custom vertical router + .with_state(state); + ``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..99fd4db --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1037 @@ +{ + "name": "stick", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stick", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.0.tgz", + "integrity": "sha512-X9kdlqyMopO9fewbgHsEeuy31YzMHbdZ9VsKt004tB+mxSg1CNbyhZYCzvhciN0AM4R4b5lvIprPjtNq7iQxpQ==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "enhanced-resolve": "^5.21.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.3.0" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e6c5211 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "stick", + "version": "1.0.0", + "description": "A production-ready Rust web application template organized by vertical features (use-cases) rather than horizontal technical layers.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@tailwindcss/cli": "^4.3.0", + "tailwindcss": "^4.3.0" + } +} diff --git a/src/auth/extractors.rs b/src/auth/extractors.rs new file mode 100644 index 0000000..08d4d26 --- /dev/null +++ b/src/auth/extractors.rs @@ -0,0 +1,87 @@ +use axum::{ + extract::{FromRef, FromRequestParts, OptionalFromRequestParts}, + http::request::Parts, +}; +use axum_extra::extract::CookieJar; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use mongodb::bson::oid::ObjectId; +use crate::common::config::Config; +use crate::common::errors::AppError; +use crate::auth::models::Claims; + +pub struct AuthenticatedUser { + pub user_id: ObjectId, + pub username: String, +} + +impl FromRequestParts for AuthenticatedUser +where + Config: FromRef, + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let config = Config::from_ref(state); + + let jar = CookieJar::from_request_parts(parts, state) + .await + .map_err(|_| AppError::Unauthorized("Failed to parse cookies".to_string()))?; + + let token = jar + .get("token") + .map(|cookie| cookie.value().to_string()) + .ok_or_else(|| AppError::Unauthorized("No authorization token found. Please sign in.".to_string()))?; + + let token_data = decode::( + &token, + &DecodingKey::from_secret(config.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(|e| AppError::Unauthorized(format!("Invalid session: {}", e)))?; + + let user_id = ObjectId::parse_str(&token_data.claims.sub) + .map_err(|_| AppError::Unauthorized("Invalid user session identifier".to_string()))?; + + Ok(AuthenticatedUser { + user_id, + username: token_data.claims.username, + }) + } +} + +impl OptionalFromRequestParts for AuthenticatedUser +where + Config: FromRef, + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result, Self::Rejection> { + let config = Config::from_ref(state); + + let jar = CookieJar::from_request_parts(parts, state) + .await + .map_err(|_| AppError::Unauthorized("Failed to parse cookies".to_string()))?; + + let Some(token_cookie) = jar.get("token") else { + return Ok(None); + }; + + let token = token_cookie.value(); + let token_data = decode::( + token, + &DecodingKey::from_secret(config.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(|e| AppError::Unauthorized(format!("Invalid session: {}", e)))?; + + let user_id = ObjectId::parse_str(&token_data.claims.sub) + .map_err(|_| AppError::Unauthorized("Invalid user session identifier".to_string()))?; + + Ok(Some(AuthenticatedUser { + user_id, + username: token_data.claims.username, + })) + } +} diff --git a/src/auth/handlers.rs b/src/auth/handlers.rs new file mode 100644 index 0000000..e3c46dc --- /dev/null +++ b/src/auth/handlers.rs @@ -0,0 +1,207 @@ +use askama::Template; +use axum::{ + extract::{Form, State}, + http::StatusCode, + response::{Html, IntoResponse, Redirect, Response}, +}; +use axum_extra::extract::{ + cookie::{Cookie, SameSite}, + CookieJar, +}; +use bcrypt::{hash, verify, DEFAULT_COST}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::Deserialize; +use time::Duration; +use crate::common::config::Config; +use crate::common::errors::AppError; +use crate::auth::models::Claims; +use crate::auth::extractors::AuthenticatedUser; +use crate::auth::repository::{UserRepository, MongoUserRepository}; + +// Wrapper for rendering Askama HTML +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => { + tracing::error!("Failed to render template: {:?}", err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +// Askama template structs +#[derive(Template)] +#[template(path = "auth/login.html")] +struct LoginTemplate { + error: Option, + authenticated: bool, + username: String, +} + +#[derive(Template)] +#[template(path = "auth/register.html")] +struct RegisterTemplate { + error: Option, + success: Option, + authenticated: bool, + username: String, +} + +// HANDLERS + +pub async fn get_login( + user_opt: Option, +) -> impl IntoResponse { + if user_opt.is_some() { + return Redirect::to("/tasks").into_response(); + } + HtmlTemplate(LoginTemplate { + error: None, + authenticated: false, + username: "".to_string(), + }) + .into_response() +} + +#[derive(Deserialize)] +pub struct AuthPayload { + pub username: String, + pub password: String, +} + +pub async fn post_login( + State(config): State, + State(user_repo): State, + jar: CookieJar, + Form(payload): Form, +) -> Result { + let username = payload.username.trim(); + + // Find user + let user = user_repo.find_by_username(username).await?; + let Some(user) = user else { + return Ok(HtmlTemplate(LoginTemplate { + error: Some("Invalid username or password".to_string()), + authenticated: false, + username: "".to_string(), + }) + .into_response()); + }; + + // Verify password + match verify(&payload.password, &user.password_hash) { + Ok(true) => {} + _ => { + return Ok(HtmlTemplate(LoginTemplate { + error: Some("Invalid username or password".to_string()), + authenticated: false, + username: "".to_string(), + }) + .into_response()); + } + } + + // Generate JWT + let exp = chrono::Utc::now() + .checked_add_signed(chrono::Duration::hours(24)) + .expect("Valid duration") + .timestamp() as usize; + + let claims = Claims { + sub: user.id.expect("User document must have ID").to_hex(), + username: user.username, + exp, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(config.jwt_secret.as_bytes()), + )?; + + // Set cookie + let cookie = Cookie::build(("token", token)) + .path("/") + .http_only(true) + .same_site(SameSite::Lax) + .max_age(Duration::days(1)); + + let updated_jar = jar.add(cookie); + + Ok((updated_jar, Redirect::to("/tasks")).into_response()) +} + +pub async fn get_register( + user_opt: Option, +) -> impl IntoResponse { + if user_opt.is_some() { + return Redirect::to("/tasks").into_response(); + } + HtmlTemplate(RegisterTemplate { + error: None, + success: None, + authenticated: false, + username: "".to_string(), + }) + .into_response() +} + +pub async fn post_register( + State(user_repo): State, + Form(payload): Form, +) -> Result { + let username = payload.username.trim(); + let password = payload.password.trim(); + + if username.is_empty() || password.is_empty() { + return Ok(HtmlTemplate(RegisterTemplate { + error: Some("Username and password cannot be empty".to_string()), + success: None, + authenticated: false, + username: "".to_string(), + }) + .into_response()); + } + + // Check if user exists + let existing_user = user_repo.find_by_username(username).await?; + if existing_user.is_some() { + return Ok(HtmlTemplate(RegisterTemplate { + error: Some("Username already taken".to_string()), + success: None, + authenticated: false, + username: "".to_string(), + }) + .into_response()); + } + + // Hash password + let hashed_password = hash(password, DEFAULT_COST)?; + + // Create user + user_repo.create(username, &hashed_password).await?; + + Ok(HtmlTemplate(RegisterTemplate { + error: None, + success: Some("Registration successful! You can now log in.".to_string()), + authenticated: false, + username: "".to_string(), + }) + .into_response()) +} + +pub async fn post_logout(jar: CookieJar) -> impl IntoResponse { + let mut cookie = Cookie::new("token", ""); + cookie.set_path("/"); + cookie.set_max_age(Some(Duration::ZERO)); // Clear cookie + + let updated_jar = jar.add(cookie); + (updated_jar, Redirect::to("/")).into_response() +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..33428e8 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,23 @@ +pub mod extractors; +pub mod handlers; +pub mod models; +pub mod repository; + +use axum::{ + routing::{get, post}, + Router, +}; +use crate::common::config::Config; +use crate::auth::repository::MongoUserRepository; + +pub fn router() -> Router +where + Config: axum::extract::FromRef, + MongoUserRepository: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, +{ + Router::new() + .route("/auth/login", get(handlers::get_login).post(handlers::post_login)) + .route("/auth/register", get(handlers::get_register).post(handlers::post_register)) + .route("/auth/logout", post(handlers::post_logout)) +} diff --git a/src/auth/models.rs b/src/auth/models.rs new file mode 100644 index 0000000..afbd7ed --- /dev/null +++ b/src/auth/models.rs @@ -0,0 +1,21 @@ +use mongodb::bson::oid::ObjectId; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct User { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub username: String, + pub password_hash: String, + #[serde_as(as = "mongodb::bson::serde_helpers::datetime::FromChrono04DateTime")] + pub created_at: chrono::DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub username: String, + pub exp: usize, +} diff --git a/src/auth/repository.rs b/src/auth/repository.rs new file mode 100644 index 0000000..11f7ee0 --- /dev/null +++ b/src/auth/repository.rs @@ -0,0 +1,52 @@ +use mongodb::{ + bson::doc, + Database, +}; +use crate::common::errors::AppError; +use crate::auth::models::User; + +#[async_trait::async_trait] +pub trait UserRepository { + async fn find_by_username(&self, username: &str) -> Result, AppError>; + async fn create(&self, username: &str, password_hash: &str) -> Result; +} + +#[derive(Clone)] +pub struct MongoUserRepository { + db: Database, +} + +impl MongoUserRepository { + pub fn new(db: Database) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl UserRepository for MongoUserRepository { + async fn find_by_username(&self, username: &str) -> Result, AppError> { + let collection = self.db.collection::("users"); + let filter = doc! { "username": username }; + + let user = collection.find_one(filter).await?; + Ok(user) + } + + async fn create(&self, username: &str, password_hash: &str) -> Result { + let collection = self.db.collection::("users"); + + let new_user = User { + id: None, + username: username.to_string(), + password_hash: password_hash.to_string(), + created_at: chrono::Utc::now(), + }; + + let insert_result = collection.insert_one(new_user.clone()).await?; + + let mut user = new_user; + user.id = Some(insert_result.inserted_id.as_object_id().expect("Inserted ID is ObjectId")); + + Ok(user) + } +} diff --git a/src/common/config.rs b/src/common/config.rs new file mode 100644 index 0000000..b9bef79 --- /dev/null +++ b/src/common/config.rs @@ -0,0 +1,42 @@ +use std::env; + +#[derive(Clone, Debug)] +pub struct Config { + pub database_url: String, + pub database_name: String, + pub jwt_secret: String, + pub host: String, + pub port: u16, +} + +impl Config { + pub fn from_env() -> Self { + // Load .env if present + let _ = dotenvy::dotenv(); + + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "mongodb://localhost:27017".to_string()); + + let database_name = env::var("DATABASE_NAME") + .unwrap_or_else(|_| "stick_db".to_string()); + + let jwt_secret = env::var("JWT_SECRET") + .unwrap_or_else(|_| "super_secret_fallback_key_for_json_web_token_signing_and_verification_must_be_long_enough".to_string()); + + let host = env::var("HOST") + .unwrap_or_else(|_| "127.0.0.1".to_string()); + + let port = env::var("PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(3000); + + Self { + database_url, + database_name, + jwt_secret, + host, + port, + } + } +} diff --git a/src/common/database.rs b/src/common/database.rs new file mode 100644 index 0000000..d1f60f9 --- /dev/null +++ b/src/common/database.rs @@ -0,0 +1,44 @@ +use mongodb::{ + bson::doc, + options::{ClientOptions, IndexOptions}, + Client, Database, IndexModel, +}; +use tracing::info; +use crate::common::config::Config; +use crate::common::errors::AppError; + +pub async fn connect_db(config: &Config) -> Result { + info!("Connecting to MongoDB at: {}", config.database_url); + + let mut client_options = ClientOptions::parse(&config.database_url).await?; + client_options.app_name = Some("stick-template".to_string()); + + let client = Client::with_options(client_options)?; + let db = client.database(&config.database_name); + + // Ping to verify connection + db.run_command(doc! { "ping": 1 }).await?; + info!("Successfully connected to database: {}", config.database_name); + + // Build index models + setup_indexes(&db).await?; + + Ok(db) +} + +async fn setup_indexes(db: &Database) -> Result<(), AppError> { + info!("Setting up database indexes..."); + + // Setup unique index for user username + let users_col = db.collection::("users"); + + let username_index = IndexModel::builder() + .keys(doc! { "username": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(); + + users_col.create_index(username_index).await?; + + info!("Database index setup completed successfully."); + Ok(()) +} diff --git a/src/common/errors.rs b/src/common/errors.rs new file mode 100644 index 0000000..19cb02f --- /dev/null +++ b/src/common/errors.rs @@ -0,0 +1,94 @@ +use axum::{ + http::StatusCode, + response::{Html, IntoResponse, Response}, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Database error: {0}")] + Mongo(#[from] mongodb::error::Error), + + #[error("Crypto error: {0}")] + Bcrypt(#[from] bcrypt::BcryptError), + + #[error("Authentication token error: {0}")] + Jwt(#[from] jsonwebtoken::errors::Error), + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("Bad Request: {0}")] + BadRequest(String), + + #[error("Internal Server Error: {0}")] + #[allow(dead_code)] + Internal(String), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, msg) = match &self { + AppError::Mongo(err) => { + tracing::error!("Database Error: {:?}", err); + (StatusCode::INTERNAL_SERVER_ERROR, "A database error occurred.") + } + AppError::Bcrypt(err) => { + tracing::error!("Password hashing error: {:?}", err); + (StatusCode::INTERNAL_SERVER_ERROR, "An authentication hashing error occurred.") + } + AppError::Jwt(err) => { + tracing::error!("Token signing/validation error: {:?}", err); + (StatusCode::UNAUTHORIZED, "Your session has expired or is invalid. Please log in again.") + } + AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.as_str()), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()), + AppError::Internal(msg) => { + tracing::error!("Internal Error: {}", msg); + (StatusCode::INTERNAL_SERVER_ERROR, msg.as_str()) + } + }; + + // Render a premium Tailwind styled error page + let html_content = format!( + r#" + + + + + Error - Template App + + + + + +
+
+
+ + + +
+

Something went wrong

+

{}

+ +
+ +"#, + msg + ); + + (status, Html(html_content)).into_response() + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..610da5e --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod database; +pub mod errors; diff --git a/src/components/handlers.rs b/src/components/handlers.rs new file mode 100644 index 0000000..3b00923 --- /dev/null +++ b/src/components/handlers.rs @@ -0,0 +1,209 @@ +use axum::response::IntoResponse; +use askama::Template; +use crate::common::errors::AppError; +use crate::auth::extractors::AuthenticatedUser; + +// Wrapper for rendering Askama HTML +pub struct HtmlTemplate(pub T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> axum::response::Response { + match self.0.render() { + Ok(html) => axum::response::Html(html).into_response(), + Err(err) => { + tracing::error!("Failed to render template: {:?}", err); + axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +// Macro helper to construct user session details +fn get_session_info(user_opt: Option) -> (bool, String) { + match user_opt { + Some(user) => (true, user.username), + None => (false, "".to_string()), + } +} + +// Define individual templates +#[derive(Template)] +#[template(path = "components/index.html")] +pub struct IndexTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/buttons.html")] +pub struct ButtonsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/inputs.html")] +pub struct InputsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/date_time.html")] +pub struct DateTimeTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/combobox.html")] +pub struct ComboboxTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/toggles.html")] +pub struct TogglesTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/modals.html")] +pub struct ModalsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/sheets.html")] +pub struct SheetsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/tabs_accordion.html")] +pub struct TabsAccordionTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/visuals.html")] +pub struct VisualsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/scrollbars.html")] +pub struct ScrollbarsTemplate { + pub username: String, + pub authenticated: bool, +} + +#[derive(Template)] +#[template(path = "components/feedback.html")] +pub struct FeedbackTemplate { + pub username: String, + pub authenticated: bool, +} + +// Define individual handlers +pub async fn index_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(IndexTemplate { username, authenticated })) +} + +pub async fn buttons_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(ButtonsTemplate { username, authenticated })) +} + +pub async fn inputs_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(InputsTemplate { username, authenticated })) +} + +pub async fn date_time_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(DateTimeTemplate { username, authenticated })) +} + +pub async fn combobox_handler( + axum::extract::State(dev_repo): axum::extract::State, + user_opt: Option, +) -> Result { + use crate::developers::repository::DeveloperRepository; + + let user_id = user_opt.as_ref().map(|u| u.user_id); + let (authenticated, username) = get_session_info(user_opt); + + if let Some(user_id) = user_id { + let _ = dev_repo.ensure_seeded(&user_id).await; + } + + Ok(HtmlTemplate(ComboboxTemplate { username, authenticated })) +} + +pub async fn toggles_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(TogglesTemplate { username, authenticated })) +} + +pub async fn modals_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(ModalsTemplate { username, authenticated })) +} + +pub async fn sheets_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(SheetsTemplate { username, authenticated })) +} + +pub async fn tabs_accordion_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(TabsAccordionTemplate { username, authenticated })) +} + +pub async fn visuals_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(VisualsTemplate { username, authenticated })) +} + +pub async fn scrollbars_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(ScrollbarsTemplate { username, authenticated })) +} + +pub async fn feedback_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = get_session_info(user_opt); + Ok(HtmlTemplate(FeedbackTemplate { username, authenticated })) +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..992a0fd --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,30 @@ +pub mod handlers; + +use axum::{routing::get, Router}; +use self::handlers::{ + index_handler, buttons_handler, inputs_handler, date_time_handler, + combobox_handler, toggles_handler, modals_handler, sheets_handler, + tabs_accordion_handler, visuals_handler, scrollbars_handler, feedback_handler, +}; + +pub fn router() -> Router +where + crate::common::config::Config: axum::extract::FromRef, + mongodb::Database: axum::extract::FromRef, + crate::developers::repository::MongoDeveloperRepository: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, +{ + Router::new() + .route("/components", get(index_handler)) + .route("/components/buttons", get(buttons_handler)) + .route("/components/inputs", get(inputs_handler)) + .route("/components/date-time", get(date_time_handler)) + .route("/components/combobox", get(combobox_handler)) + .route("/components/toggles", get(toggles_handler)) + .route("/components/modals", get(modals_handler)) + .route("/components/sheets", get(sheets_handler)) + .route("/components/tabs-accordion", get(tabs_accordion_handler)) + .route("/components/visuals", get(visuals_handler)) + .route("/components/scrollbars", get(scrollbars_handler)) + .route("/components/feedback", get(feedback_handler)) +} diff --git a/src/developers/handlers.rs b/src/developers/handlers.rs new file mode 100644 index 0000000..43b1004 --- /dev/null +++ b/src/developers/handlers.rs @@ -0,0 +1,207 @@ +use askama::Template; +use axum::{ + extract::{Form, Path, Query, State}, + http::StatusCode, + response::{Html, IntoResponse, Redirect, Response}, +}; +use mongodb::bson::oid::ObjectId; +use serde::Deserialize; +use crate::common::errors::AppError; +use crate::auth::extractors::AuthenticatedUser; +use crate::developers::models::Developer; +use crate::developers::repository::{DeveloperRepository, MongoDeveloperRepository}; + +// Wrapper for rendering Askama HTML +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => { + tracing::error!("Failed to render template: {:?}", err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +// Askama templates +#[derive(Template)] +#[template(path = "developers/list.html")] +struct DeveloperListTemplate { + username: String, + authenticated: bool, + developers: Vec, +} + +#[derive(Template)] +#[template(path = "developers/edit.html")] +struct DeveloperEditTemplate { + username: String, + authenticated: bool, + developer: Developer, +} + +#[derive(Template)] +#[template(path = "developers/search_results.html")] +struct DeveloperSearchResultsTemplate { + developers: Vec, +} + +// HANDLERS + +pub async fn get_list( + State(dev_repo): State, + user_opt: Option, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let _ = dev_repo.ensure_seeded(&user.user_id).await; + let developers = dev_repo.find_all_by_user(&user.user_id).await?; + + Ok(HtmlTemplate(DeveloperListTemplate { + username: user.username, + authenticated: true, + developers, + }) + .into_response()) +} + +#[derive(Deserialize)] +pub struct CreateDevForm { + pub name: String, + pub email: String, + pub skills: String, +} + +pub async fn post_create( + State(dev_repo): State, + user_opt: Option, + Form(payload): Form, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let name = payload.name.trim(); + let email = payload.email.trim(); + if name.is_empty() { + return Err(AppError::BadRequest("Developer name cannot be empty".to_string())); + } + + let skills: Vec = payload.skills + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + dev_repo.create(&user.user_id, name, email, skills).await?; + + Ok(Redirect::to("/developers").into_response()) +} + +pub async fn get_edit( + State(dev_repo): State, + user_opt: Option, + Path(dev_id_str): Path, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let dev_id = ObjectId::parse_str(&dev_id_str) + .map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?; + + let developer = dev_repo.find_by_id(&dev_id, &user.user_id).await?; + let Some(developer) = developer else { + return Err(AppError::Unauthorized("Developer not found".to_string())); + }; + + Ok(HtmlTemplate(DeveloperEditTemplate { + username: user.username, + authenticated: true, + developer, + }) + .into_response()) +} + +pub async fn post_update( + State(dev_repo): State, + user_opt: Option, + Path(dev_id_str): Path, + Form(payload): Form, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let dev_id = ObjectId::parse_str(&dev_id_str) + .map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?; + + let name = payload.name.trim(); + let email = payload.email.trim(); + if name.is_empty() { + return Err(AppError::BadRequest("Developer name cannot be empty".to_string())); + } + + let skills: Vec = payload.skills + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + dev_repo.update(&dev_id, &user.user_id, name, email, skills).await?; + + Ok(Redirect::to("/developers").into_response()) +} + +pub async fn post_delete( + State(dev_repo): State, + user_opt: Option, + Path(dev_id_str): Path, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let dev_id = ObjectId::parse_str(&dev_id_str) + .map_err(|_| AppError::BadRequest("Invalid developer identifier".to_string()))?; + + dev_repo.delete(&dev_id, &user.user_id).await?; + + Ok(Redirect::to("/developers").into_response()) +} + +#[derive(Deserialize)] +pub struct SearchQuery { + pub q: String, +} + +pub async fn get_search( + State(dev_repo): State, + user_opt: Option, + Query(params): Query, +) -> Result { + let Some(user) = user_opt else { + return Ok(StatusCode::UNAUTHORIZED.into_response()); + }; + + let _ = dev_repo.ensure_seeded(&user.user_id).await; + let query_str = params.q.trim(); + if query_str.is_empty() { + return Ok(HtmlTemplate(DeveloperSearchResultsTemplate { developers: vec![] }).into_response()); + } + + let matched_devs = dev_repo.search_by_name(&user.user_id, query_str).await?; + + Ok(HtmlTemplate(DeveloperSearchResultsTemplate { + developers: matched_devs, + }) + .into_response()) +} diff --git a/src/developers/mod.rs b/src/developers/mod.rs new file mode 100644 index 0000000..c54863e --- /dev/null +++ b/src/developers/mod.rs @@ -0,0 +1,23 @@ +pub mod handlers; +pub mod models; +pub mod repository; + +use axum::{ + routing::{get, post}, + Router, +}; +use crate::common::config::Config; +use crate::developers::repository::MongoDeveloperRepository; + +pub fn router() -> Router +where + Config: axum::extract::FromRef, + MongoDeveloperRepository: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, +{ + Router::new() + .route("/developers", get(handlers::get_list).post(handlers::post_create)) + .route("/developers/{id}/edit", get(handlers::get_edit).post(handlers::post_update)) + .route("/developers/{id}/delete", post(handlers::post_delete)) + .route("/developers/search", get(handlers::get_search)) +} diff --git a/src/developers/models.rs b/src/developers/models.rs new file mode 100644 index 0000000..fc9271e --- /dev/null +++ b/src/developers/models.rs @@ -0,0 +1,16 @@ +use mongodb::bson::oid::ObjectId; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Developer { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub user_id: ObjectId, + pub name: String, + pub email: String, + pub skills: Vec, + #[serde_as(as = "mongodb::bson::serde_helpers::datetime::FromChrono04DateTime")] + pub created_at: chrono::DateTime, +} diff --git a/src/developers/repository.rs b/src/developers/repository.rs new file mode 100644 index 0000000..b9a7fe9 --- /dev/null +++ b/src/developers/repository.rs @@ -0,0 +1,123 @@ +use futures::stream::TryStreamExt; +use mongodb::{ + bson::{doc, oid::ObjectId}, + options::FindOptions, + Database, +}; +use crate::common::errors::AppError; +use crate::developers::models::Developer; + +#[async_trait::async_trait] +pub trait DeveloperRepository { + async fn find_all_by_user(&self, user_id: &ObjectId) -> Result, AppError>; + async fn find_by_id(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result, AppError>; + async fn search_by_name(&self, user_id: &ObjectId, query: &str) -> Result, AppError>; + async fn create(&self, user_id: &ObjectId, name: &str, email: &str, skills: Vec) -> Result; + async fn update(&self, dev_id: &ObjectId, user_id: &ObjectId, name: &str, email: &str, skills: Vec) -> Result<(), AppError>; + async fn delete(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>; + async fn ensure_seeded(&self, user_id: &ObjectId) -> Result<(), AppError>; +} + +#[derive(Clone)] +pub struct MongoDeveloperRepository { + db: Database, +} + +impl MongoDeveloperRepository { + pub fn new(db: Database) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl DeveloperRepository for MongoDeveloperRepository { + async fn find_all_by_user(&self, user_id: &ObjectId) -> Result, AppError> { + let collection = self.db.collection::("developers"); + let filter = doc! { "user_id": user_id }; + let find_options = FindOptions::builder().sort(doc! { "created_at": -1 }).build(); + + let mut cursor = collection.find(filter).with_options(find_options).await?; + let mut developers = Vec::new(); + while let Some(dev) = cursor.try_next().await? { + developers.push(dev); + } + Ok(developers) + } + + async fn find_by_id(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result, AppError> { + let collection = self.db.collection::("developers"); + let filter = doc! { "_id": dev_id, "user_id": user_id }; + let dev = collection.find_one(filter).await?; + Ok(dev) + } + + async fn search_by_name(&self, user_id: &ObjectId, query: &str) -> Result, AppError> { + let collection = self.db.collection::("developers"); + // Regex search case-insensitive on developer name + let filter = doc! { + "user_id": user_id, + "name": { "$regex": query, "$options": "i" } + }; + let find_options = FindOptions::builder().limit(10).build(); + + let mut cursor = collection.find(filter).with_options(find_options).await?; + let mut developers = Vec::new(); + while let Some(dev) = cursor.try_next().await? { + developers.push(dev); + } + Ok(developers) + } + + async fn create(&self, user_id: &ObjectId, name: &str, email: &str, skills: Vec) -> Result { + let collection = self.db.collection::("developers"); + let new_dev = Developer { + id: None, + user_id: *user_id, + name: name.to_string(), + email: email.to_string(), + skills, + created_at: chrono::Utc::now(), + }; + let insert_result = collection.insert_one(new_dev.clone()).await?; + let mut dev = new_dev; + dev.id = Some(insert_result.inserted_id.as_object_id().expect("Inserted ID is ObjectId")); + Ok(dev) + } + + async fn update(&self, dev_id: &ObjectId, user_id: &ObjectId, name: &str, email: &str, skills: Vec) -> Result<(), AppError> { + let collection = self.db.collection::("developers"); + let filter = doc! { "_id": dev_id, "user_id": user_id }; + let update = doc! { + "$set": { + "name": name, + "email": email, + "skills": skills + } + }; + let result = collection.update_one(filter, update).await?; + if result.matched_count == 0 { + return Err(AppError::Unauthorized("Developer not found or not owned by user".to_string())); + } + Ok(()) + } + + async fn delete(&self, dev_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError> { + let collection = self.db.collection::("developers"); + let filter = doc! { "_id": dev_id, "user_id": user_id }; + let result = collection.delete_one(filter).await?; + if result.deleted_count == 0 { + return Err(AppError::Unauthorized("Developer not found or not owned by user".to_string())); + } + Ok(()) + } + + async fn ensure_seeded(&self, user_id: &ObjectId) -> Result<(), AppError> { + let devs = self.find_all_by_user(user_id).await?; + if devs.is_empty() { + let _ = self.create(user_id, "Alice Vance", "alice@example.com", vec!["Rust".to_string(), "Axum".to_string()]).await; + let _ = self.create(user_id, "Bob Carter", "bob@example.com", vec!["Tailwind".to_string(), "JavaScript".to_string()]).await; + let _ = self.create(user_id, "Charlie Smith", "charlie@example.com", vec!["HTML".to_string(), "CSS".to_string()]).await; + } + Ok(()) + } +} diff --git a/src/input.css b/src/input.css new file mode 100644 index 0000000..c0b429e --- /dev/null +++ b/src/input.css @@ -0,0 +1,185 @@ +@import "tailwindcss"; + +@theme { + --font-sans: 'Outfit', sans-serif; + + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); +} + +html, body { + color-scheme: dark; +} + +:root { + color-scheme: dark; + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + + --radius: 0.5rem; +} + +/* Custom Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) transparent; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-button { + display: none; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 9999px; + border: 1px solid transparent; + background-clip: padding-box; + transition: background-color 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground)); +} + +/* Custom Date/Time Inputs */ +input[type="date"], +input[type="time"], +input[type="datetime-local"] { + position: relative; + padding-left: 0.5rem !important; + padding-right: 1.5rem !important; + color-scheme: dark; + background-color: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + color: hsl(var(--foreground)); + font-family: inherit; +} + +::-webkit-datetime-edit { + padding: 0 !important; + flex-shrink: 0 !important; +} + +::-webkit-datetime-edit-fields-wrapper { + padding: 0 !important; + flex-shrink: 0 !important; +} + +::-webkit-calendar-picker-indicator { + position: absolute; + right: 0.4rem; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease; +} + +::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} + +::-webkit-datetime-edit-text { + color: hsl(var(--muted-foreground)); + padding: 0 0.125rem; +} + +::-webkit-datetime-edit-month-field:focus, +::-webkit-datetime-edit-day-field:focus, +::-webkit-datetime-edit-year-field:focus { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + outline: none; + border-radius: 2px; +} + +/* Number Spinner Removal */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type=number] { + -moz-appearance: textfield; +} + +/* Custom Select Arrows */ +select { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='hsl(215.4, 16.3%, 56.9%)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 1rem; + padding-right: 2.5rem; +} + +/* Hide scrollbars for premium list picker layouts */ +.scrollbar-none { + scrollbar-width: none; /* Firefox */ +} +.scrollbar-none::-webkit-scrollbar { + display: none; /* Chrome, Safari, Edge */ +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2451ad7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,107 @@ +mod common; +mod auth; +mod tasks; +mod developers; +mod main_view; +mod components; + +use axum::{extract::FromRef, Router}; +use std::net::SocketAddr; +use tracing::info; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::common::config::Config; +use crate::common::database::connect_db; +use crate::auth::repository::MongoUserRepository; +use crate::tasks::repository::MongoTaskRepository; +use crate::developers::repository::MongoDeveloperRepository; + +#[derive(Clone)] +struct AppState { + config: Config, + db: mongodb::Database, + user_repo: MongoUserRepository, + task_repo: MongoTaskRepository, + dev_repo: MongoDeveloperRepository, +} + +impl FromRef for Config { + fn from_ref(state: &AppState) -> Self { + state.config.clone() + } +} + +impl FromRef for mongodb::Database { + fn from_ref(state: &AppState) -> Self { + state.db.clone() + } +} + +impl FromRef for MongoUserRepository { + fn from_ref(state: &AppState) -> Self { + state.user_repo.clone() + } +} + +impl FromRef for MongoTaskRepository { + fn from_ref(state: &AppState) -> Self { + state.task_repo.clone() + } +} + +impl FromRef for MongoDeveloperRepository { + fn from_ref(state: &AppState) -> Self { + state.dev_repo.clone() + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 1. Initialize logging + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .init(); + + info!("Starting Stick Template application..."); + + // 2. Parse config from env + let config = Config::from_env(); + + // 3. Connect to MongoDB + let db = connect_db(&config).await?; + + // 4. Initialize repositories + let user_repo = MongoUserRepository::new(db.clone()); + let task_repo = MongoTaskRepository::new(db.clone()); + let dev_repo = MongoDeveloperRepository::new(db.clone()); + + // 5. Initialize shared AppState + let state = AppState { + config: config.clone(), + db, + user_repo, + task_repo, + dev_repo, + }; + + // 6. Build and merge routers by use-case + let app = Router::new() + .merge(main_view::router()) + .merge(components::router()) + .merge(auth::router()) + .merge(tasks::router()) + .merge(developers::router()) + .with_state(state); + + // 7. Bind address and run server + let host_addr: SocketAddr = format!("{}:{}", config.host, config.port) + .parse() + .expect("Invalid HOST or PORT config"); + + info!("Listening on http://{}", host_addr); + + let listener = tokio::net::TcpListener::bind(host_addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/main_view/mod.rs b/src/main_view/mod.rs new file mode 100644 index 0000000..bf6a592 --- /dev/null +++ b/src/main_view/mod.rs @@ -0,0 +1,79 @@ +use axum::{routing::get, Router}; +use crate::common::errors::AppError; +use crate::auth::extractors::AuthenticatedUser; +use axum::response::IntoResponse; +use askama::Template; + +// Define the template struct extending base.html +#[derive(Template)] +#[template(path = "main_view/index.html")] +struct IndexTemplate { + username: String, + authenticated: bool, +} + +async fn index_handler( + user_opt: Option, +) -> Result { + let (authenticated, username) = match user_opt { + Some(user) => (true, user.username), + None => (false, "".to_string()), + }; + + Ok(HtmlTemplate(IndexTemplate { username, authenticated })) +} + +// Wrapper for rendering Askama HTML +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> axum::response::Response { + match self.0.render() { + Ok(html) => axum::response::Html(html).into_response(), + Err(err) => { + tracing::error!("Failed to render template: {:?}", err); + axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +async fn serve_tailwind() -> impl IntoResponse { + let css = include_str!("../../static/tailwind.css"); + ( + [(axum::http::header::CONTENT_TYPE, "text/css")], + css + ) +} + +async fn serve_combobox_js() -> impl IntoResponse { + let js = include_str!("../../static/js/combobox.js"); + ( + [(axum::http::header::CONTENT_TYPE, "application/javascript")], + js + ) +} + +async fn serve_components_js() -> impl IntoResponse { + let js = include_str!("../../static/js/components.js"); + ( + [(axum::http::header::CONTENT_TYPE, "application/javascript")], + js + ) +} + +pub fn router() -> Router +where + crate::common::config::Config: axum::extract::FromRef, + mongodb::Database: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, +{ + Router::new() + .route("/", get(index_handler)) + .route("/static/tailwind.css", get(serve_tailwind)) + .route("/static/js/combobox.js", get(serve_combobox_js)) + .route("/static/js/components.js", get(serve_components_js)) +} diff --git a/src/tasks/handlers.rs b/src/tasks/handlers.rs new file mode 100644 index 0000000..e49039e --- /dev/null +++ b/src/tasks/handlers.rs @@ -0,0 +1,150 @@ +use askama::Template; +use axum::{ + extract::{Form, Path, State}, + http::StatusCode, + response::{Html, IntoResponse, Redirect, Response}, +}; +use mongodb::bson::oid::ObjectId; +use serde::Deserialize; +use crate::common::errors::AppError; +use crate::auth::extractors::AuthenticatedUser; +use crate::tasks::models::Task; +use crate::tasks::repository::{TaskRepository, MongoTaskRepository}; +use crate::developers::repository::{DeveloperRepository, MongoDeveloperRepository}; + +// Wrapper for rendering Askama HTML +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => { + tracing::error!("Failed to render template: {:?}", err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } + } + } +} + +pub struct TaskViewModel { + pub task: Task, + pub developer_name: Option, +} + +// Askama template struct +#[derive(Template)] +#[template(path = "tasks/dashboard.html")] +struct DashboardTemplate { + username: String, + authenticated: bool, + tasks: Vec, +} + +// HANDLERS + +pub async fn get_dashboard( + State(task_repo): State, + State(dev_repo): State, + user_opt: Option, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let _ = dev_repo.ensure_seeded(&user.user_id).await; + let tasks = task_repo.find_all_by_user(&user.user_id).await?; + let mut task_vms = Vec::new(); + for task in tasks { + let developer_name = if let Some(dev_id) = task.assigned_to { + dev_repo.find_by_id(&dev_id, &user.user_id).await?.map(|d| d.name) + } else { + None + }; + task_vms.push(TaskViewModel { + task, + developer_name, + }); + } + + Ok(HtmlTemplate(DashboardTemplate { + username: user.username, + authenticated: true, + tasks: task_vms, + }) + .into_response()) +} + +#[derive(Deserialize)] +pub struct CreateTaskForm { + pub title: String, + pub description: Option, + pub assignee_id: Option, +} + +pub async fn post_create_task( + State(task_repo): State, + user_opt: Option, + Form(payload): Form, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let title = payload.title.trim(); + if title.is_empty() { + return Err(AppError::BadRequest("Task title cannot be empty".to_string())); + } + + let description = payload.description.as_deref().map(|d| d.trim()); + + let assigned_to = match payload.assignee_id.as_deref() { + Some(id_str) if !id_str.trim().is_empty() => { + ObjectId::parse_str(id_str.trim()).ok() + } + _ => None, + }; + + task_repo + .create(&user.user_id, title, description, assigned_to) + .await?; + + Ok(Redirect::to("/tasks").into_response()) +} + +pub async fn post_complete_task( + State(task_repo): State, + user_opt: Option, + Path(task_id_str): Path, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let task_id = ObjectId::parse_str(&task_id_str) + .map_err(|_| AppError::BadRequest("Invalid task identifier".to_string()))?; + + task_repo.mark_completed(&task_id, &user.user_id).await?; + + Ok(Redirect::to("/tasks").into_response()) +} + +pub async fn post_delete_task( + State(task_repo): State, + user_opt: Option, + Path(task_id_str): Path, +) -> Result { + let Some(user) = user_opt else { + return Ok(Redirect::to("/auth/login").into_response()); + }; + + let task_id = ObjectId::parse_str(&task_id_str) + .map_err(|_| AppError::BadRequest("Invalid task identifier".to_string()))?; + + task_repo.delete(&task_id, &user.user_id).await?; + + Ok(Redirect::to("/tasks").into_response()) +} diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs new file mode 100644 index 0000000..f7a6128 --- /dev/null +++ b/src/tasks/mod.rs @@ -0,0 +1,24 @@ +pub mod handlers; +pub mod models; +pub mod repository; + +use axum::{ + routing::{get, post}, + Router, +}; +use crate::common::config::Config; +use crate::tasks::repository::MongoTaskRepository; + +pub fn router() -> Router +where + Config: axum::extract::FromRef, + MongoTaskRepository: axum::extract::FromRef, + crate::developers::repository::MongoDeveloperRepository: axum::extract::FromRef, + S: Clone + Send + Sync + 'static, +{ + Router::new() + .route("/tasks", get(handlers::get_dashboard)) + .route("/tasks/create", post(handlers::post_create_task)) + .route("/tasks/{id}/complete", post(handlers::post_complete_task)) + .route("/tasks/{id}/delete", post(handlers::post_delete_task)) +} diff --git a/src/tasks/models.rs b/src/tasks/models.rs new file mode 100644 index 0000000..0264a71 --- /dev/null +++ b/src/tasks/models.rs @@ -0,0 +1,18 @@ +use mongodb::bson::oid::ObjectId; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Task { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub user_id: ObjectId, + pub title: String, + pub description: Option, + pub is_completed: bool, + #[serde_as(as = "mongodb::bson::serde_helpers::datetime::FromChrono04DateTime")] + pub created_at: chrono::DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub assigned_to: Option, +} diff --git a/src/tasks/repository.rs b/src/tasks/repository.rs new file mode 100644 index 0000000..3e63cd5 --- /dev/null +++ b/src/tasks/repository.rs @@ -0,0 +1,99 @@ +use futures::stream::TryStreamExt; +use mongodb::{ + bson::{doc, oid::ObjectId}, + options::FindOptions, + Database, +}; +use crate::common::errors::AppError; +use crate::tasks::models::Task; + +#[async_trait::async_trait] +pub trait TaskRepository { + async fn find_all_by_user(&self, user_id: &ObjectId) -> Result, AppError>; + async fn create(&self, user_id: &ObjectId, title: &str, description: Option<&str>, assigned_to: Option) -> Result; + async fn mark_completed(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>; + async fn delete(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError>; +} + +#[derive(Clone)] +pub struct MongoTaskRepository { + db: Database, +} + +impl MongoTaskRepository { + pub fn new(db: Database) -> Self { + Self { db } + } +} + +#[async_trait::async_trait] +impl TaskRepository for MongoTaskRepository { + async fn find_all_by_user(&self, user_id: &ObjectId) -> Result, AppError> { + let collection = self.db.collection::("tasks"); + + let filter = doc! { "user_id": user_id }; + + // Sort incomplete tasks first, and then order by creation timestamp descending + let find_options = FindOptions::builder() + .sort(doc! { "is_completed": 1, "created_at": -1 }) + .build(); + + let mut cursor = collection.find(filter).with_options(find_options).await?; + let mut tasks = Vec::new(); + + while let Some(task) = cursor.try_next().await? { + tasks.push(task); + } + + Ok(tasks) + } + + async fn create(&self, user_id: &ObjectId, title: &str, description: Option<&str>, assigned_to: Option) -> Result { + let collection = self.db.collection::("tasks"); + + let new_task = Task { + id: None, + user_id: *user_id, + title: title.to_string(), + description: description.map(|d| d.to_string()), + is_completed: false, + created_at: chrono::Utc::now(), + assigned_to, + }; + + let insert_result = collection.insert_one(new_task.clone()).await?; + + let mut task = new_task; + task.id = Some(insert_result.inserted_id.as_object_id().expect("Inserted ID is ObjectId")); + + Ok(task) + } + + async fn mark_completed(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError> { + let collection = self.db.collection::("tasks"); + + let filter = doc! { "_id": task_id, "user_id": user_id }; + let update = doc! { "$set": { "is_completed": true } }; + + let result = collection.update_one(filter, update).await?; + + if result.matched_count == 0 { + return Err(AppError::Unauthorized("Task not found or not owned by user".to_string())); + } + + Ok(()) + } + + async fn delete(&self, task_id: &ObjectId, user_id: &ObjectId) -> Result<(), AppError> { + let collection = self.db.collection::("tasks"); + + let filter = doc! { "_id": task_id, "user_id": user_id }; + let result = collection.delete_one(filter).await?; + + if result.deleted_count == 0 { + return Err(AppError::Unauthorized("Task not found or not owned by user".to_string())); + } + + Ok(()) + } +} diff --git a/static/js/combobox.js b/static/js/combobox.js new file mode 100644 index 0000000..bc0ca92 --- /dev/null +++ b/static/js/combobox.js @@ -0,0 +1,201 @@ +document.addEventListener('DOMContentLoaded', () => { + + // 1. Close all comboboxes when clicking outside + document.addEventListener('click', (event) => { + document.querySelectorAll('.autocomplete-combobox').forEach(combo => { + const results = combo.querySelector('.combobox-results'); + if (results && !combo.contains(event.target)) { + results.classList.add('hidden'); + } + }); + }); + + // Helper to get only currently visible combobox items + function getVisibleItems(combo) { + const results = combo.querySelector('.combobox-results'); + if (!results || results.classList.contains('hidden')) { + return []; + } + return Array.from(results.querySelectorAll('.combobox-item')).filter(item => { + return !item.classList.contains('hidden') && item.offsetParent !== null; + }); + } + + // Helper to show dropdown results + function showDropdown(input) { + const combo = input.closest('.autocomplete-combobox'); + if (!combo) return; + if (combo.dataset.justSelected === 'true') return; + + const results = combo.querySelector('.combobox-results'); + if (!results) return; + + if (!input.hasAttribute('hx-get')) { + // Client-side: filter and display + results.classList.remove('hidden'); + const query = input.value.toLowerCase().trim(); + const items = results.querySelectorAll('.combobox-item'); + let visibleCount = 0; + + items.forEach(item => { + const text = item.getAttribute('data-name').toLowerCase(); + if (text.includes(query)) { + item.classList.remove('hidden'); + visibleCount++; + } else { + item.classList.add('hidden'); + } + }); + + if (visibleCount === 0 && query !== '') { + results.classList.add('hidden'); + } + } else { + // Server-side HTMX search: only show if results contain elements + if (results.querySelector('.combobox-item') || results.querySelector('div')) { + results.classList.remove('hidden'); + } + } + } + + // 2. Open dropdown on focusin and click + document.addEventListener('focusin', (event) => { + const input = event.target.closest('.combobox-input'); + if (input) { + showDropdown(input); + } + }); + + document.addEventListener('click', (event) => { + const input = event.target.closest('.combobox-input'); + if (input) { + showDropdown(input); + } + }); + + // 3. Clear values on input delete and perform client-side filtering + document.addEventListener('input', (event) => { + const input = event.target.closest('.combobox-input'); + if (!input) return; + + const combo = input.closest('.autocomplete-combobox'); + const valueInput = combo.querySelector('.combobox-value'); + const results = combo.querySelector('.combobox-results'); + + if (input.value.trim() === '') { + if (valueInput) valueInput.value = ''; + if (results) results.classList.add('hidden'); + return; + } + + // Apply client-side search filtering immediately + if (!input.hasAttribute('hx-get')) { + showDropdown(input); + } + }); + + // 4. Keydown navigation delegation (Arrows, Escape, Enter, Tab/Shift-Tab) + document.addEventListener('keydown', (event) => { + const input = event.target.closest('.combobox-input'); + const item = event.target.closest('.combobox-item'); + + if (input) { + const combo = input.closest('.autocomplete-combobox'); + const results = combo.querySelector('.combobox-results'); + + if (event.key === 'Escape') { + if (results) results.classList.add('hidden'); + input.blur(); + } else if (event.key === 'ArrowDown' || (event.key === 'Tab' && !event.shiftKey)) { + const visibleItems = getVisibleItems(combo); + if (visibleItems.length > 0) { + event.preventDefault(); + visibleItems[0].focus(); + } + } + return; + } + + if (item) { + const combo = item.closest('.autocomplete-combobox'); + const results = combo.querySelector('.combobox-results'); + const inputField = combo.querySelector('.combobox-input'); + const visibleItems = getVisibleItems(combo); + const index = visibleItems.indexOf(item); + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + selectItem(combo, item); + } else if (event.key === 'ArrowDown' || (event.key === 'Tab' && !event.shiftKey)) { + event.preventDefault(); + if (index >= 0 && index + 1 < visibleItems.length) { + visibleItems[index + 1].focus(); + } else { + // Loop back to first item + if (visibleItems.length > 0) visibleItems[0].focus(); + } + } else if (event.key === 'ArrowUp' || (event.key === 'Tab' && event.shiftKey)) { + event.preventDefault(); + if (index > 0) { + visibleItems[index - 1].focus(); + } else if (inputField) { + // Return focus back to search input + inputField.focus(); + } + } else if (event.key === 'Escape') { + event.preventDefault(); + if (results) results.classList.add('hidden'); + if (inputField) inputField.focus(); + } + } + }); + + // 5. Click event delegation for selection + document.addEventListener('click', (event) => { + const item = event.target.closest('.combobox-item'); + if (item) { + const combo = item.closest('.autocomplete-combobox'); + selectItem(combo, item); + } + }); + + // 6. HTMX Swap Integration to show results + document.addEventListener('htmx:afterSwap', (event) => { + const results = event.target.querySelector('.combobox-results') + || event.target.closest('.combobox-results'); + if (results) { + results.classList.remove('hidden'); + } + }); + + // Core helper to perform selection swap + function selectItem(combo, item) { + const input = combo.querySelector('.combobox-input'); + const valueInput = combo.querySelector('.combobox-value'); + const results = combo.querySelector('.combobox-results'); + + const id = item.getAttribute('data-id'); + const name = item.getAttribute('data-name'); + + if (valueInput) { + valueInput.value = id; + valueInput.dispatchEvent(new Event('change', { bubbles: true })); + } + + if (input) { + input.value = name; + input.focus(); + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + } + + if (results) { + results.classList.add('hidden'); + // Prevent immediate reopening on focus + combo.dataset.justSelected = 'true'; + setTimeout(() => { + delete combo.dataset.justSelected; + }, 200); + } + } +}); diff --git a/static/js/components.js b/static/js/components.js new file mode 100644 index 0000000..67b5eb1 --- /dev/null +++ b/static/js/components.js @@ -0,0 +1,669 @@ +document.addEventListener('DOMContentLoaded', () => { + + // --- DIALOG / MODAL --- + window.openModal = function(modalId) { + const modal = document.getElementById(modalId); + if (!modal) return; + modal.classList.remove('hidden'); + setTimeout(() => { + const content = modal.querySelector('.modal-content'); + if (content) { + content.classList.remove('scale-95', 'opacity-0'); + content.classList.add('scale-100', 'opacity-100'); + } + }, 10); + }; + + window.closeModal = function(modal) { + if (typeof modal === 'string') modal = document.getElementById(modal); + if (!modal) return; + const content = modal.querySelector('.modal-content'); + if (content) { + content.classList.remove('scale-100', 'opacity-100'); + content.classList.add('scale-95', 'opacity-0'); + } + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); // Wait for transition animation + }; + + document.addEventListener('click', (event) => { + // Modal Trigger Buttons + const trigger = event.target.closest('[data-modal-target]'); + if (trigger) { + const targetId = trigger.getAttribute('data-modal-target'); + window.openModal(targetId); + } + + // Modal Close Buttons + if (event.target.closest('.modal-close') || event.target.closest('.modal-backdrop')) { + const modal = event.target.closest('.modal-dialog'); + window.closeModal(modal); + } + }); + + // --- DROPDOWNS --- + document.addEventListener('click', (event) => { + const trigger = event.target.closest('.dropdown-trigger'); + + // Close all other dropdowns + document.querySelectorAll('.dropdown-menu').forEach(menu => { + const content = menu.querySelector('.dropdown-content'); + if (content && (!trigger || menu !== trigger.closest('.dropdown-menu'))) { + content.classList.add('hidden'); + } + }); + + // Toggle selected dropdown + if (trigger) { + const menu = trigger.closest('.dropdown-menu'); + const content = menu.querySelector('.dropdown-content'); + if (content) content.classList.toggle('hidden'); + } + }); + + // --- TOAST NOTIFICATIONS --- + window.showToast = function(message, type = 'success') { + const container = document.getElementById('toast-container'); + if (!container) return; + + const toast = document.createElement('div'); + toast.className = `toast-item flex items-center gap-3 w-80 p-4 rounded-lg border border-border bg-card shadow-lg transform transition-all duration-300 translate-y-10 opacity-0`; + toast.innerHTML = ` +
${message}
+ + `; + container.appendChild(toast); + + // Slide in + setTimeout(() => { + toast.classList.remove('translate-y-10', 'opacity-0'); + }, 10); + + // Auto-dismiss after 4 seconds + const dismissTimeout = setTimeout(() => dismissToast(toast), 4000); + + // Manual dismiss listener + toast.querySelector('.toast-close').addEventListener('click', () => { + clearTimeout(dismissTimeout); + dismissToast(toast); + }); + }; + + function dismissToast(toast) { + toast.classList.add('translate-y-10', 'opacity-0'); + setTimeout(() => { + toast.remove(); + }, 300); + } + + // --- CUSTOM DATE PICKER LOGIC --- + function formatDateLabel(dateStr) { + if (!dateStr) return 'Pick a date'; + const parts = dateStr.split('-'); + if (parts.length !== 3) return dateStr; + const d = new Date(parts[0], parts[1] - 1, parts[2]); + const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + return `${monthNames[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; + } + + function renderCalendar(picker) { + const valueInput = picker.querySelector('.datepicker-value'); + const monthYearLabel = picker.querySelector('.datepicker-month-year'); + const daysContainer = picker.querySelector('.datepicker-days'); + if (!valueInput || !monthYearLabel || !daysContainer) return; + + let currentYear = parseInt(picker.dataset.year); + let currentMonth = parseInt(picker.dataset.month); // 0-indexed + + if (isNaN(currentYear) || isNaN(currentMonth)) { + const val = valueInput.value; + const d = val ? new Date(val) : new Date(); + currentYear = d.getFullYear(); + currentMonth = d.getMonth(); + picker.dataset.year = currentYear; + picker.dataset.month = currentMonth; + } + + const selectedDateVal = valueInput.value; + const today = new Date(); + + const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + monthYearLabel.textContent = `${monthNames[currentMonth]} ${currentYear}`; + + daysContainer.innerHTML = ''; + + const firstDayIndex = new Date(currentYear, currentMonth, 1).getDay(); + const totalDays = new Date(currentYear, currentMonth + 1, 0).getDate(); + const prevTotalDays = new Date(currentYear, currentMonth, 0).getDate(); + + // Prev month padding + for (let i = firstDayIndex - 1; i >= 0; i--) { + const dayNum = prevTotalDays - i; + const btn = document.createElement('button'); + btn.type = 'button'; + btn.disabled = true; + btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; + btn.textContent = dayNum; + daysContainer.appendChild(btn); + } + + // Current month days + for (let day = 1; day <= totalDays; day++) { + const btn = document.createElement('button'); + btn.type = 'button'; + const yearStr = currentYear; + const monthStr = String(currentMonth + 1).padStart(2, '0'); + const dayStr = String(day).padStart(2, '0'); + const dateStr = `${yearStr}-${monthStr}-${dayStr}`; + + const isSelected = selectedDateVal === dateStr; + const isToday = today.getFullYear() === currentYear && today.getMonth() === currentMonth && today.getDate() === day; + + btn.className = `py-1 text-xs rounded-lg font-semibold hover:bg-accent hover:text-accent-foreground transition flex items-center justify-center h-7 w-7 mx-auto ${ + isSelected ? 'bg-indigo-600 text-white hover:bg-indigo-650' : + isToday ? 'border border-sky-500/50 text-sky-400' : 'text-slate-350' + }`; + btn.dataset.date = dateStr; + btn.textContent = day; + daysContainer.appendChild(btn); + } + + // Next month padding + const totalGrids = 42; + const currentGrids = firstDayIndex + totalDays; + for (let i = 1; i <= (totalGrids - currentGrids); i++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.disabled = true; + btn.className = 'py-1 text-center text-xs text-slate-700 cursor-not-allowed font-medium'; + btn.textContent = i; + daysContainer.appendChild(btn); + } + } + + // Initialize custom date pickers + document.querySelectorAll('.custom-datepicker').forEach(picker => { + renderCalendar(picker); + const val = picker.querySelector('.datepicker-value').value; + const text = picker.querySelector('.datepicker-text'); + if (text && val) text.textContent = formatDateLabel(val); + }); + + // Custom Date Picker events + document.addEventListener('click', (event) => { + // Trigger Click + const trigger = event.target.closest('.datepicker-trigger'); + if (trigger) { + const picker = trigger.closest('.custom-datepicker'); + const popover = picker.querySelector('.datepicker-popover'); + + // Close other pickers + document.querySelectorAll('.datepicker-popover, .timepicker-popover').forEach(pop => { + if (pop !== popover) pop.classList.add('hidden'); + }); + + popover.classList.toggle('hidden'); + return; + } + + // Prev Month + const prevBtn = event.target.closest('.datepicker-prev'); + if (prevBtn) { + const picker = prevBtn.closest('.custom-datepicker'); + let currentMonth = parseInt(picker.dataset.month); + let currentYear = parseInt(picker.dataset.year); + currentMonth--; + if (currentMonth < 0) { + currentMonth = 11; + currentYear--; + } + picker.dataset.month = currentMonth; + picker.dataset.year = currentYear; + renderCalendar(picker); + return; + } + + // Next Month + const nextBtn = event.target.closest('.datepicker-next'); + if (nextBtn) { + const picker = nextBtn.closest('.custom-datepicker'); + let currentMonth = parseInt(picker.dataset.month); + let currentYear = parseInt(picker.dataset.year); + currentMonth++; + if (currentMonth > 11) { + currentMonth = 0; + currentYear++; + } + picker.dataset.month = currentMonth; + picker.dataset.year = currentYear; + renderCalendar(picker); + return; + } + + // Day click + const dayBtn = event.target.closest('.datepicker-days button[data-date]'); + if (dayBtn) { + const picker = dayBtn.closest('.custom-datepicker'); + const valueInput = picker.querySelector('.datepicker-value'); + const textLabel = picker.querySelector('.datepicker-text'); + const popover = picker.querySelector('.datepicker-popover'); + + const selectedDate = dayBtn.dataset.date; + valueInput.value = selectedDate; + textLabel.textContent = formatDateLabel(selectedDate); + renderCalendar(picker); + popover.classList.add('hidden'); + valueInput.dispatchEvent(new Event('change', { bubbles: true })); + return; + } + + // Close outside + const openPickerPopover = document.querySelector('.datepicker-popover:not(.hidden)'); + if (openPickerPopover && !event.target.closest('.custom-datepicker')) { + openPickerPopover.classList.add('hidden'); + } + }); + + // --- CUSTOM TIME PICKER LOGIC --- + function initTimePicker(picker) { + const hoursCol = picker.querySelector('.timepicker-col-hours'); + const minutesCol = picker.querySelector('.timepicker-col-minutes'); + const valueInput = picker.querySelector('.timepicker-value'); + if (!hoursCol || !minutesCol || !valueInput) return; + + if (hoursCol.children.length === 0) { + for (let hr = 1; hr <= 12; hr++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'timepicker-btn-hour block w-full py-1.5 text-xs hover:bg-accent hover:text-accent-foreground transition text-slate-350 font-bold rounded-md'; + btn.textContent = hr; + hoursCol.appendChild(btn); + } + } + + if (minutesCol.children.length === 0) { + for (let min = 0; min < 60; min += 5) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'timepicker-btn-minute block w-full py-1.5 text-xs hover:bg-accent hover:text-accent-foreground transition text-slate-350 font-bold rounded-md'; + btn.textContent = String(min).padStart(2, '0'); + minutesCol.appendChild(btn); + } + } + + updateTimePickerActiveStates(picker); + } + + function updateTimePickerActiveStates(picker) { + const valueInput = picker.querySelector('.timepicker-value'); + const textLabel = picker.querySelector('.timepicker-text'); + if (!valueInput || !textLabel) return; + + const val = valueInput.value || "12:00 PM"; + const matches = val.match(/^(\d+):(\d+)\s*(AM|PM)$/i); + if (!matches) return; + + const hr = parseInt(matches[1]); + const min = matches[2]; + const ampm = matches[3].toUpperCase(); + + picker.querySelectorAll('.timepicker-btn-hour').forEach(btn => { + if (parseInt(btn.textContent) === hr) { + btn.className = 'timepicker-btn-hour block w-full py-1.5 text-xs bg-indigo-600 text-white font-bold rounded-md'; + } else { + btn.className = 'timepicker-btn-hour block w-full py-1.5 text-xs hover:bg-accent hover:text-accent-foreground transition text-slate-350 font-bold rounded-md'; + } + }); + + picker.querySelectorAll('.timepicker-btn-minute').forEach(btn => { + if (btn.textContent === min) { + btn.className = 'timepicker-btn-minute block w-full py-1.5 text-xs bg-indigo-600 text-white font-bold rounded-md'; + } else { + btn.className = 'timepicker-btn-minute block w-full py-1.5 text-xs hover:bg-accent hover:text-accent-foreground transition text-slate-350 font-bold rounded-md'; + } + }); + + picker.querySelectorAll('.timepicker-ampm-btn').forEach(btn => { + if (btn.textContent.toUpperCase() === ampm) { + btn.className = 'timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold bg-indigo-650 text-white'; + } else { + btn.className = 'timepicker-ampm-btn py-1.5 rounded-lg text-xs font-bold hover:bg-accent hover:text-accent-foreground transition text-slate-400'; + } + }); + + textLabel.textContent = `${hr}:${min} ${ampm}`; + } + + document.querySelectorAll('.custom-timepicker').forEach(picker => { + initTimePicker(picker); + }); + + document.addEventListener('click', (event) => { + const trigger = event.target.closest('.timepicker-trigger'); + if (trigger) { + const picker = trigger.closest('.custom-timepicker'); + const popover = picker.querySelector('.timepicker-popover'); + + document.querySelectorAll('.datepicker-popover, .timepicker-popover').forEach(pop => { + if (pop !== popover) pop.classList.add('hidden'); + }); + + popover.classList.toggle('hidden'); + + if (!popover.classList.contains('hidden')) { + setTimeout(() => { + const activeHour = picker.querySelector('.timepicker-btn-hour.bg-indigo-600'); + const activeMin = picker.querySelector('.timepicker-btn-minute.bg-indigo-600'); + if (activeHour) activeHour.scrollIntoView({ block: 'center', behavior: 'auto' }); + if (activeMin) activeMin.scrollIntoView({ block: 'center', behavior: 'auto' }); + }, 10); + } + return; + } + + const btnHour = event.target.closest('.timepicker-btn-hour'); + const btnMinute = event.target.closest('.timepicker-btn-minute'); + const btnAmpm = event.target.closest('.timepicker-ampm-btn'); + + if (btnHour || btnMinute || btnAmpm) { + const picker = event.target.closest('.custom-timepicker'); + if (!picker) return; + + const valueInput = picker.querySelector('.timepicker-value'); + let val = valueInput.value || "12:00 PM"; + const matches = val.match(/^(\d+):(\d+)\s*(AM|PM)$/i); + if (!matches) return; + + let hr = matches[1]; + let min = matches[2]; + let ampm = matches[3]; + + if (btnHour) hr = btnHour.textContent; + if (btnMinute) min = btnMinute.textContent; + if (btnAmpm) ampm = btnAmpm.textContent; + + valueInput.value = `${hr}:${min} ${ampm}`; + updateTimePickerActiveStates(picker); + valueInput.dispatchEvent(new Event('change', { bubbles: true })); + return; + } + + const openTimePopover = document.querySelector('.timepicker-popover:not(.hidden)'); + if (openTimePopover && !event.target.closest('.custom-timepicker')) { + openTimePopover.classList.add('hidden'); + } + }); + + // --- INTERACTIVE TABS LOGIC --- + document.addEventListener('click', (event) => { + const tabTrigger = event.target.closest('[data-tab-target]'); + if (tabTrigger) { + const tabGroup = tabTrigger.getAttribute('data-tab-group'); + const targetId = tabTrigger.getAttribute('data-tab-target'); + if (!tabGroup || !targetId) return; + + // Deactivate all tab triggers in the same group + document.querySelectorAll(`[data-tab-group="${tabGroup}"]`).forEach(trig => { + trig.classList.remove('border-sky-500', 'text-sky-400'); + trig.classList.add('border-transparent', 'text-slate-400', 'hover:text-slate-200'); + }); + + // Activate selected tab trigger + tabTrigger.classList.remove('border-transparent', 'text-slate-400', 'hover:text-slate-200'); + tabTrigger.classList.add('border-sky-500', 'text-sky-400'); + + // Hide all tab content panes in the same group + document.querySelectorAll(`[data-tab-content-group="${tabGroup}"]`).forEach(pane => { + pane.classList.add('hidden'); + }); + + // Show the targeted pane + const targetPane = document.getElementById(targetId); + if (targetPane) targetPane.classList.remove('hidden'); + } + }); + + // --- ACCORDION LOGIC --- + document.addEventListener('click', (event) => { + const header = event.target.closest('.accordion-trigger'); + if (header) { + const container = header.closest('.accordion-item'); + const content = container.querySelector('.accordion-content'); + const chevron = header.querySelector('.accordion-chevron'); + if (!content) return; + + const isCollapsed = content.classList.contains('hidden'); + + // Toggle Accordion Content + if (isCollapsed) { + content.classList.remove('hidden'); + if (chevron) chevron.classList.add('rotate-180'); + } else { + content.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + } + } + }); + + // --- SHEET / DRAWER LOGIC --- + window.openSheet = function(sheetId) { + const sheet = document.getElementById(sheetId); + if (!sheet) return; + sheet.classList.remove('hidden'); + setTimeout(() => { + const content = sheet.querySelector('.sheet-content'); + const backdrop = sheet.querySelector('.sheet-backdrop'); + if (backdrop) backdrop.classList.remove('opacity-0'); + if (content) { + content.classList.remove('translate-x-full'); + content.classList.add('translate-x-0'); + } + }, 10); + }; + + window.closeSheet = function(sheet) { + if (typeof sheet === 'string') sheet = document.getElementById(sheet); + if (!sheet) return; + const content = sheet.querySelector('.sheet-content'); + const backdrop = sheet.querySelector('.sheet-backdrop'); + if (backdrop) backdrop.classList.add('opacity-0'); + if (content) { + content.classList.remove('translate-x-0'); + content.classList.add('translate-x-full'); + } + setTimeout(() => { + sheet.classList.add('hidden'); + }, 300); + }; + + document.addEventListener('click', (event) => { + // Open Sheet Triggers + const trigger = event.target.closest('[data-sheet-target]'); + if (trigger) { + const targetId = trigger.getAttribute('data-sheet-target'); + window.openSheet(targetId); + } + + // Close Sheet Triggers + if (event.target.closest('.sheet-close') || event.target.closest('.sheet-backdrop')) { + const sheet = event.target.closest('.sheet-dialog'); + window.closeSheet(sheet); + } + }); + + // --- SIDEBAR NAVIGATION ACTIVE HIGHLIGHTING --- + const currentPath = window.location.pathname; + const sidebar = document.getElementById('wiki-sidebar'); + if (sidebar) { + sidebar.querySelectorAll('a[data-wiki-path]').forEach(link => { + const path = link.getAttribute('data-wiki-path'); + if (currentPath === path) { + link.className = 'flex items-center px-3 py-2 text-xs font-semibold rounded-lg bg-indigo-600/20 text-indigo-400 border border-indigo-500/10 transition'; + } else { + link.className = 'flex items-center px-3 py-2 text-xs font-semibold text-muted-foreground hover:text-white rounded-lg hover:bg-accent hover:text-accent-foreground transition'; + } + }); + } + + // --- CUSTOM SELECT DROPDOWN LOGIC --- + document.addEventListener('click', (event) => { + const trigger = event.target.closest('.select-trigger'); + const item = event.target.closest('.select-item'); + + // Close all other select dropdowns when clicking trigger or outside + if (trigger || !event.target.closest('.custom-select')) { + document.querySelectorAll('.custom-select').forEach(select => { + const popover = select.querySelector('.select-popover'); + const chevron = select.querySelector('.select-chevron'); + if (popover && (!trigger || select !== trigger.closest('.custom-select'))) { + popover.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + } + }); + } + + // Toggle dropdown open state + if (trigger) { + const select = trigger.closest('.custom-select'); + const popover = select.querySelector('.select-popover'); + const chevron = select.querySelector('.select-chevron'); + if (popover) { + const isHidden = popover.classList.toggle('hidden'); + if (chevron) { + if (isHidden) { + chevron.classList.remove('rotate-180'); + } else { + chevron.classList.add('rotate-180'); + } + } + } + } + + // Handle item selection + if (item) { + const select = item.closest('.custom-select'); + const triggerBtn = select.querySelector('.select-trigger'); + const valueInput = select.querySelector('.select-value'); + const textLabel = select.querySelector('.select-text'); + const popover = select.querySelector('.select-popover'); + const chevron = select.querySelector('.select-chevron'); + + const value = item.getAttribute('data-value'); + const labelText = item.textContent.trim(); + + if (valueInput) valueInput.value = value; + if (textLabel) textLabel.textContent = labelText; + + // Mark selected item visually + select.querySelectorAll('.select-item').forEach(i => { + i.classList.remove('bg-accent', 'text-accent-foreground', 'font-semibold'); + }); + item.classList.add('bg-accent', 'text-accent-foreground', 'font-semibold'); + + // Close dropdown + if (popover) popover.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + if (triggerBtn) triggerBtn.focus(); + + // Trigger change event on input + if (valueInput) { + valueInput.dispatchEvent(new Event('change', { bubbles: true })); + } + } + }); + + // Keyboard support for custom select triggers + document.addEventListener('keydown', (event) => { + const trigger = event.target.closest('.select-trigger'); + const item = event.target.closest('.select-item'); + + if (trigger) { + const select = trigger.closest('.custom-select'); + const popover = select.querySelector('.select-popover'); + + if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + if (popover) { + popover.classList.remove('hidden'); + const chevron = select.querySelector('.select-chevron'); + if (chevron) chevron.classList.add('rotate-180'); + } + const firstItem = select.querySelector('.select-item'); + if (firstItem) firstItem.focus(); + } + return; + } + + if (item) { + const select = item.closest('.custom-select'); + const triggerBtn = select.querySelector('.select-trigger'); + const items = Array.from(select.querySelectorAll('.select-item')); + const index = items.indexOf(item); + + if (event.key === 'ArrowDown') { + event.preventDefault(); + const next = items[index + 1] || items[0]; + if (next) next.focus(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + const prev = items[index - 1] || items[items.length - 1]; + if (prev) prev.focus(); + } else if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + item.click(); + } else if (event.key === 'Escape') { + event.preventDefault(); + const popover = select.querySelector('.select-popover'); + const chevron = select.querySelector('.select-chevron'); + if (popover) popover.classList.add('hidden'); + if (chevron) chevron.classList.remove('rotate-180'); + if (triggerBtn) triggerBtn.focus(); + } + } + }); + + // Global keydown listener for Escape to dismiss modals + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + const openDialog = document.querySelector('.modal-dialog:not(.hidden)'); + if (openDialog) { + window.closeModal(openDialog); + } + } + }); + + // --- COPY CODE SNIPPETS HELPER --- + window.copyCodeSnippet = function(button) { + const pre = button.closest('.relative').querySelector('pre'); + if (!pre) return; + const code = pre.querySelector('code'); + const text = code ? code.innerText : pre.innerText; + + navigator.clipboard.writeText(text).then(() => { + const originalText = button.innerHTML; + button.innerHTML = ` + + Copied! + `; + button.classList.add('border-sky-500/30', 'bg-sky-500/5'); + if (window.showToast) { + window.showToast('Snippet copied to clipboard!'); + } + setTimeout(() => { + button.innerHTML = originalText; + button.classList.remove('border-sky-500/30', 'bg-sky-500/5'); + }, 2000); + }).catch(err => { + console.error('Failed to copy text: ', err); + if (window.showToast) { + window.showToast('Failed to copy to clipboard', 'error'); + } + }); + }; +}); + + + diff --git a/static/tailwind.css b/static/tailwind.css new file mode 100644 index 0000000..4ea7df3 --- /dev/null +++ b/static/tailwind.css @@ -0,0 +1,2799 @@ +/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: 'Outfit', sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --color-red-500: oklch(63.7% 0.237 25.331); + --color-amber-400: oklch(82.8% 0.189 84.429); + --color-amber-500: oklch(76.9% 0.188 70.08); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); + --color-teal-500: oklch(70.4% 0.14 182.503); + --color-teal-600: oklch(60% 0.118 184.704); + --color-cyan-600: oklch(60.9% 0.126 221.723); + --color-sky-400: oklch(74.6% 0.16 232.661); + --color-sky-500: oklch(68.5% 0.169 237.323); + --color-sky-900: oklch(39.1% 0.09 240.876); + --color-sky-950: oklch(29.3% 0.066 243.157); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-indigo-400: oklch(67.3% 0.182 276.935); + --color-indigo-500: oklch(58.5% 0.233 277.117); + --color-indigo-600: oklch(51.1% 0.262 276.966); + --color-pink-600: oklch(59.2% 0.249 0.584); + --color-rose-300: oklch(81% 0.117 11.638); + --color-rose-400: oklch(71.2% 0.194 13.428); + --color-rose-500: oklch(64.5% 0.246 16.439); + --color-rose-900: oklch(41% 0.159 10.272); + --color-rose-950: oklch(27.1% 0.105 12.094); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-slate-950: oklch(12.9% 0.042 264.695); + --color-white: #fff; + --spacing: 0.25rem; + --container-xs: 20rem; + --container-sm: 24rem; + --container-md: 28rem; + --container-lg: 32rem; + --container-2xl: 42rem; + --container-4xl: 56rem; + --container-5xl: 64rem; + --container-7xl: 80rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + --tracking-tight: -0.025em; + --tracking-wider: 0.05em; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: 0.75rem; + --radius-2xl: 1rem; + --radius-3xl: 1.5rem; + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --animate-spin: spin 1s linear infinite; + --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + --blur-sm: 8px; + --blur-md: 12px; + --blur-xl: 24px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-popover: hsl(var(--popover)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-border: hsl(var(--border)); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .visible { + visibility: visible; + } + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .sticky { + position: sticky; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .inset-y-0 { + inset-block: calc(var(--spacing) * 0); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-2 { + top: calc(var(--spacing) * 2); + } + .top-3 { + top: calc(var(--spacing) * 3); + } + .top-24 { + top: calc(var(--spacing) * 24); + } + .right-0 { + right: calc(var(--spacing) * 0); + } + .right-2 { + right: calc(var(--spacing) * 2); + } + .right-4 { + right: calc(var(--spacing) * 4); + } + .bottom-4 { + bottom: calc(var(--spacing) * 4); + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .left-3 { + left: calc(var(--spacing) * 3); + } + .z-10 { + z-index: 10; + } + .z-20 { + z-index: 20; + } + .z-50 { + z-index: 50; + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .mx-2 { + margin-inline: calc(var(--spacing) * 2); + } + .mx-auto { + margin-inline: auto; + } + .my-1 { + margin-block: calc(var(--spacing) * 1); + } + .mt-0\.5 { + margin-top: calc(var(--spacing) * 0.5); + } + .mt-1 { + margin-top: calc(var(--spacing) * 1); + } + .mt-1\.5 { + margin-top: calc(var(--spacing) * 1.5); + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .mt-6 { + margin-top: calc(var(--spacing) * 6); + } + .mb-1 { + margin-bottom: calc(var(--spacing) * 1); + } + .mb-1\.5 { + margin-bottom: calc(var(--spacing) * 1.5); + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } + .mb-3\.5 { + margin-bottom: calc(var(--spacing) * 3.5); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .mb-10 { + margin-bottom: calc(var(--spacing) * 10); + } + .mb-12 { + margin-bottom: calc(var(--spacing) * 12); + } + .mb-16 { + margin-bottom: calc(var(--spacing) * 16); + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } + .block { + display: block; + } + .contents { + display: contents; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .inline-block { + display: inline-block; + } + .inline-flex { + display: inline-flex; + } + .h-1 { + height: calc(var(--spacing) * 1); + } + .h-1\.5 { + height: calc(var(--spacing) * 1.5); + } + .h-2\.5 { + height: calc(var(--spacing) * 2.5); + } + .h-3 { + height: calc(var(--spacing) * 3); + } + .h-3\.5 { + height: calc(var(--spacing) * 3.5); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-4\.5 { + height: calc(var(--spacing) * 4.5); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-7 { + height: calc(var(--spacing) * 7); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-9 { + height: calc(var(--spacing) * 9); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-16 { + height: calc(var(--spacing) * 16); + } + .h-32 { + height: calc(var(--spacing) * 32); + } + .h-full { + height: 100%; + } + .h-px { + height: 1px; + } + .max-h-28 { + max-height: calc(var(--spacing) * 28); + } + .max-h-60 { + max-height: calc(var(--spacing) * 60); + } + .min-h-\[5rem\] { + min-height: 5rem; + } + .min-h-screen { + min-height: 100vh; + } + .w-1 { + width: calc(var(--spacing) * 1); + } + .w-1\.5 { + width: calc(var(--spacing) * 1.5); + } + .w-1\/3 { + width: calc(1 / 3 * 100%); + } + .w-1\/4 { + width: calc(1 / 4 * 100%); + } + .w-1\/6 { + width: calc(1 / 6 * 100%); + } + .w-2\.5 { + width: calc(var(--spacing) * 2.5); + } + .w-3 { + width: calc(var(--spacing) * 3); + } + .w-3\.5 { + width: calc(var(--spacing) * 3.5); + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-4\.5 { + width: calc(var(--spacing) * 4.5); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-7 { + width: calc(var(--spacing) * 7); + } + .w-8 { + width: calc(var(--spacing) * 8); + } + .w-9 { + width: calc(var(--spacing) * 9); + } + .w-10 { + width: calc(var(--spacing) * 10); + } + .w-12 { + width: calc(var(--spacing) * 12); + } + .w-16 { + width: calc(var(--spacing) * 16); + } + .w-80 { + width: calc(var(--spacing) * 80); + } + .w-\[230px\] { + width: 230px; + } + .w-\[270px\] { + width: 270px; + } + .w-full { + width: 100%; + } + .w-px { + width: 1px; + } + .w-screen { + width: 100vw; + } + .max-w-2xl { + max-width: var(--container-2xl); + } + .max-w-4xl { + max-width: var(--container-4xl); + } + .max-w-5xl { + max-width: var(--container-5xl); + } + .max-w-7xl { + max-width: var(--container-7xl); + } + .max-w-full { + max-width: 100%; + } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-md { + max-width: var(--container-md); + } + .max-w-sm { + max-width: var(--container-sm); + } + .max-w-xs { + max-width: var(--container-xs); + } + .flex-1 { + flex: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .shrink-0 { + flex-shrink: 0; + } + .grow { + flex-grow: 1; + } + .border-collapse { + border-collapse: collapse; + } + .translate-x-0 { + --tw-translate-x: calc(var(--spacing) * 0); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-x-full { + --tw-translate-x: 100%; + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .translate-y-10 { + --tw-translate-y: calc(var(--spacing) * 10); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + .scale-95 { + --tw-scale-x: 95%; + --tw-scale-y: 95%; + --tw-scale-z: 95%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .scale-100 { + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .rotate-180 { + rotate: 180deg; + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .animate-pulse { + animation: var(--animate-pulse); + } + .animate-spin { + animation: var(--animate-spin); + } + .cursor-not-allowed { + cursor: not-allowed; + } + .cursor-pointer { + cursor: pointer; + } + .resize-none { + resize: none; + } + .scrollbar-none { + scrollbar-width: none; + } + .list-disc { + list-style-type: disc; + } + .appearance-none { + appearance: none; + } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + .grid-cols-7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .items-start { + align-items: flex-start; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-1 { + gap: calc(var(--spacing) * 1); + } + .gap-1\.5 { + gap: calc(var(--spacing) * 1.5); + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-2\.5 { + gap: calc(var(--spacing) * 2.5); + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .gap-8 { + gap: calc(var(--spacing) * 8); + } + .space-y-0\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 0.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 0.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-1 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-1\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-2\.5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2.5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-5 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-6 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-8 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-10 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse))); + } + } + .gap-x-3 { + column-gap: calc(var(--spacing) * 3); + } + .space-x-2\.5 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 2.5) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 2.5) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-3 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-4 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } + .gap-y-1\.5 { + row-gap: calc(var(--spacing) * 1.5); + } + .divide-y { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + .divide-border\/40 { + :where(& > :not(:last-child)) { + border-color: color-mix(in srgb, hsl(var(--border)) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-border) 40%, transparent); + } + } + } + .self-end { + align-self: flex-end; + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-auto { + overflow-x: auto; + } + .overflow-y-auto { + overflow-y: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-2xl { + border-radius: var(--radius-2xl); + } + .rounded-3xl { + border-radius: var(--radius-3xl); + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-md { + border-radius: var(--radius-md); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-b-2 { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 2px; + } + .border-l { + border-left-style: var(--tw-border-style); + border-left-width: 1px; + } + .border-dashed { + --tw-border-style: dashed; + border-style: dashed; + } + .border-amber-500\/20 { + border-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-amber-500) 20%, transparent); + } + } + .border-border { + border-color: var(--color-border); + } + .border-border\/60 { + border-color: color-mix(in srgb, hsl(var(--border)) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-border) 60%, transparent); + } + } + .border-emerald-500\/20 { + border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); + } + } + .border-emerald-500\/30 { + border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-emerald-500) 30%, transparent); + } + } + .border-emerald-500\/50 { + border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-emerald-500) 50%, transparent); + } + } + .border-indigo-500\/10 { + border-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-indigo-500) 10%, transparent); + } + } + .border-indigo-500\/20 { + border-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-indigo-500) 20%, transparent); + } + } + .border-rose-500\/20 { + border-color: color-mix(in srgb, oklch(64.5% 0.246 16.439) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-rose-500) 20%, transparent); + } + } + .border-rose-900\/50 { + border-color: color-mix(in srgb, oklch(41% 0.159 10.272) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-rose-900) 50%, transparent); + } + } + .border-sky-500 { + border-color: var(--color-sky-500); + } + .border-sky-500\/20 { + border-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-sky-500) 20%, transparent); + } + } + .border-sky-500\/30 { + border-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-sky-500) 30%, transparent); + } + } + .border-sky-500\/50 { + border-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-sky-500) 50%, transparent); + } + } + .border-sky-900\/30 { + border-color: color-mix(in srgb, oklch(39.1% 0.09 240.876) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-sky-900) 30%, transparent); + } + } + .border-slate-700 { + border-color: var(--color-slate-700); + } + .border-slate-800 { + border-color: var(--color-slate-800); + } + .border-slate-900 { + border-color: var(--color-slate-900); + } + .border-slate-950 { + border-color: var(--color-slate-950); + } + .border-transparent { + border-color: transparent; + } + .bg-\[\#0f172a\] { + background-color: #0f172a; + } + .bg-\[\#0f172a\]\/80 { + background-color: color-mix(in oklab, #0f172a 80%, transparent); + } + .bg-\[\#1e293b\] { + background-color: #1e293b; + } + .bg-\[\#1e293b\]\/30 { + background-color: color-mix(in oklab, #1e293b 30%, transparent); + } + .bg-\[\#1e293b\]\/40 { + background-color: color-mix(in oklab, #1e293b 40%, transparent); + } + .bg-\[\#07090e\]\/80 { + background-color: color-mix(in oklab, #07090e 80%, transparent); + } + .bg-\[\#09090b\]\/40 { + background-color: color-mix(in oklab, #09090b 40%, transparent); + } + .bg-\[\#030712\] { + background-color: #030712; + } + .bg-\[\#f8fafc\] { + background-color: #f8fafc; + } + .bg-accent { + background-color: var(--color-accent); + } + .bg-amber-500\/5 { + background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-amber-500) 5%, transparent); + } + } + .bg-amber-500\/10 { + background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-amber-500) 10%, transparent); + } + } + .bg-background { + background-color: var(--color-background); + } + .bg-background\/80 { + background-color: color-mix(in srgb, hsl(var(--background)) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-background) 80%, transparent); + } + } + .bg-card { + background-color: var(--color-card); + } + .bg-card\/30 { + background-color: color-mix(in srgb, hsl(var(--card)) 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-card) 30%, transparent); + } + } + .bg-card\/40 { + background-color: color-mix(in srgb, hsl(var(--card)) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-card) 40%, transparent); + } + } + .bg-card\/50 { + background-color: color-mix(in srgb, hsl(var(--card)) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-card) 50%, transparent); + } + } + .bg-destructive { + background-color: var(--color-destructive); + } + .bg-emerald-500\/10 { + background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-500) 10%, transparent); + } + } + .bg-emerald-500\/20 { + background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); + } + } + .bg-indigo-500\/5 { + background-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-indigo-500) 5%, transparent); + } + } + .bg-indigo-500\/10 { + background-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-indigo-500) 10%, transparent); + } + } + .bg-indigo-600 { + background-color: var(--color-indigo-600); + } + .bg-indigo-600\/20 { + background-color: color-mix(in srgb, oklch(51.1% 0.262 276.966) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-indigo-600) 20%, transparent); + } + } + .bg-popover { + background-color: var(--color-popover); + } + .bg-popover\/80 { + background-color: color-mix(in srgb, hsl(var(--popover)) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-popover) 80%, transparent); + } + } + .bg-popover\/95 { + background-color: color-mix(in srgb, hsl(var(--popover)) 95%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-popover) 95%, transparent); + } + } + .bg-primary { + background-color: var(--color-primary); + } + .bg-rose-500\/10 { + background-color: color-mix(in srgb, oklch(64.5% 0.246 16.439) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-rose-500) 10%, transparent); + } + } + .bg-rose-950\/50 { + background-color: color-mix(in srgb, oklch(27.1% 0.105 12.094) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-rose-950) 50%, transparent); + } + } + .bg-secondary { + background-color: var(--color-secondary); + } + .bg-secondary\/10 { + background-color: color-mix(in srgb, hsl(var(--secondary)) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-secondary) 10%, transparent); + } + } + .bg-sky-400 { + background-color: var(--color-sky-400); + } + .bg-sky-500\/5 { + background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-sky-500) 5%, transparent); + } + } + .bg-sky-500\/10 { + background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-sky-500) 10%, transparent); + } + } + .bg-sky-950\/40 { + background-color: color-mix(in srgb, oklch(29.3% 0.066 243.157) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-sky-950) 40%, transparent); + } + } + .bg-slate-700 { + background-color: var(--color-slate-700); + } + .bg-slate-800 { + background-color: var(--color-slate-800); + } + .bg-slate-900 { + background-color: var(--color-slate-900); + } + .bg-slate-900\/20 { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 20%, transparent); + } + } + .bg-slate-900\/40 { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 40%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 40%, transparent); + } + } + .bg-slate-900\/60 { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 60%, transparent); + } + } + .bg-transparent { + background-color: transparent; + } + .bg-linear-to-r { + --tw-gradient-position: to right; + @supports (background-image: linear-gradient(in lab, red, red)) { + --tw-gradient-position: to right in oklab; + } + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .bg-gradient-to-r { + --tw-gradient-position: to right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .bg-gradient-to-tr { + --tw-gradient-position: to top right in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .from-emerald-400 { + --tw-gradient-from: var(--color-emerald-400); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-emerald-400\/20 { + --tw-gradient-from: color-mix(in srgb, oklch(76.5% 0.177 163.223) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-emerald-400) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-emerald-500 { + --tw-gradient-from: var(--color-emerald-500); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-red-500 { + --tw-gradient-from: var(--color-red-500); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-sky-400 { + --tw-gradient-from: var(--color-sky-400); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-sky-400\/20 { + --tw-gradient-from: color-mix(in srgb, oklch(74.6% 0.16 232.661) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-from: color-mix(in oklab, var(--color-sky-400) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-sky-500 { + --tw-gradient-from: var(--color-sky-500); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .from-slate-100 { + --tw-gradient-from: var(--color-slate-100); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .via-blue-500 { + --tw-gradient-via: var(--color-blue-500); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .via-blue-600 { + --tw-gradient-via: var(--color-blue-600); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .via-rose-500 { + --tw-gradient-via: var(--color-rose-500); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .via-slate-200 { + --tw-gradient-via: var(--color-slate-200); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .via-teal-500 { + --tw-gradient-via: var(--color-teal-500); + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .to-cyan-600 { + --tw-gradient-to: var(--color-cyan-600); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-indigo-500 { + --tw-gradient-to: var(--color-indigo-500); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-indigo-500\/20 { + --tw-gradient-to: color-mix(in srgb, oklch(58.5% 0.233 277.117) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-indigo-500) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-indigo-600 { + --tw-gradient-to: var(--color-indigo-600); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-pink-600 { + --tw-gradient-to: var(--color-pink-600); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-slate-300 { + --tw-gradient-to: var(--color-slate-300); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-slate-400 { + --tw-gradient-to: var(--color-slate-400); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-teal-500\/20 { + --tw-gradient-to: color-mix(in srgb, oklch(70.4% 0.14 182.503) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-to: color-mix(in oklab, var(--color-teal-500) 20%, transparent); + } + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .to-teal-600 { + --tw-gradient-to: var(--color-teal-600); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .bg-clip-text { + background-clip: text; + } + .p-0\.5 { + padding: calc(var(--spacing) * 0.5); + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-3 { + padding: calc(var(--spacing) * 3); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-5 { + padding: calc(var(--spacing) * 5); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .p-12 { + padding: calc(var(--spacing) * 12); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-2\.5 { + padding-inline: calc(var(--spacing) * 2.5); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } + .px-8 { + padding-inline: calc(var(--spacing) * 8); + } + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-1\.5 { + padding-block: calc(var(--spacing) * 1.5); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-2\.5 { + padding-block: calc(var(--spacing) * 2.5); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-3\.5 { + padding-block: calc(var(--spacing) * 3.5); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .py-6 { + padding-block: calc(var(--spacing) * 6); + } + .py-10 { + padding-block: calc(var(--spacing) * 10); + } + .py-12 { + padding-block: calc(var(--spacing) * 12); + } + .py-20 { + padding-block: calc(var(--spacing) * 20); + } + .pt-1 { + padding-top: calc(var(--spacing) * 1); + } + .pt-2 { + padding-top: calc(var(--spacing) * 2); + } + .pt-6 { + padding-top: calc(var(--spacing) * 6); + } + .pt-8 { + padding-top: calc(var(--spacing) * 8); + } + .pr-1\.5 { + padding-right: calc(var(--spacing) * 1.5); + } + .pr-4 { + padding-right: calc(var(--spacing) * 4); + } + .pb-1\.5 { + padding-bottom: calc(var(--spacing) * 1.5); + } + .pb-2\.5 { + padding-bottom: calc(var(--spacing) * 2.5); + } + .pb-3 { + padding-bottom: calc(var(--spacing) * 3); + } + .pb-6 { + padding-bottom: calc(var(--spacing) * 6); + } + .pl-5 { + padding-left: calc(var(--spacing) * 5); + } + .pl-9 { + padding-left: calc(var(--spacing) * 9); + } + .pl-10 { + padding-left: calc(var(--spacing) * 10); + } + .text-center { + text-align: center; + } + .text-left { + text-align: left; + } + .text-right { + text-align: right; + } + .font-mono { + font-family: var(--font-mono); + } + .font-sans { + font-family: var(--font-sans); + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .text-\[9px\] { + font-size: 9px; + } + .text-\[10px\] { + font-size: 10px; + } + .text-\[11px\] { + font-size: 11px; + } + .leading-none { + --tw-leading: 1; + line-height: 1; + } + .leading-normal { + --tw-leading: var(--leading-normal); + line-height: var(--leading-normal); + } + .leading-relaxed { + --tw-leading: var(--leading-relaxed); + line-height: var(--leading-relaxed); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-extrabold { + --tw-font-weight: var(--font-weight-extrabold); + font-weight: var(--font-weight-extrabold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .tracking-wider { + --tw-tracking: var(--tracking-wider); + letter-spacing: var(--tracking-wider); + } + .text-accent-foreground { + color: var(--color-accent-foreground); + } + .text-amber-400 { + color: var(--color-amber-400); + } + .text-amber-500 { + color: var(--color-amber-500); + } + .text-destructive-foreground { + color: var(--color-destructive-foreground); + } + .text-emerald-400 { + color: var(--color-emerald-400); + } + .text-foreground { + color: var(--color-foreground); + } + .text-indigo-400 { + color: var(--color-indigo-400); + } + .text-muted-foreground { + color: var(--color-muted-foreground); + } + .text-muted-foreground\/70 { + color: color-mix(in srgb, hsl(var(--muted-foreground)) 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-muted-foreground) 70%, transparent); + } + } + .text-muted-foreground\/90 { + color: color-mix(in srgb, hsl(var(--muted-foreground)) 90%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-muted-foreground) 90%, transparent); + } + } + .text-primary-foreground { + color: var(--color-primary-foreground); + } + .text-rose-400 { + color: var(--color-rose-400); + } + .text-rose-500 { + color: var(--color-rose-500); + } + .text-secondary-foreground { + color: var(--color-secondary-foreground); + } + .text-sky-400 { + color: var(--color-sky-400); + } + .text-sky-500 { + color: var(--color-sky-500); + } + .text-slate-100 { + color: var(--color-slate-100); + } + .text-slate-200 { + color: var(--color-slate-200); + } + .text-slate-300 { + color: var(--color-slate-300); + } + .text-slate-400 { + color: var(--color-slate-400); + } + .text-slate-500 { + color: var(--color-slate-500); + } + .text-slate-600 { + color: var(--color-slate-600); + } + .text-slate-700 { + color: var(--color-slate-700); + } + .text-transparent { + color: transparent; + } + .text-white { + color: var(--color-white); + } + .uppercase { + text-transform: uppercase; + } + .line-through { + text-decoration-line: line-through; + } + .placeholder-slate-500 { + &::placeholder { + color: var(--color-slate-500); + } + } + .placeholder-slate-600 { + &::placeholder { + color: var(--color-slate-600); + } + } + .accent-indigo-600 { + accent-color: var(--color-indigo-600); + } + .opacity-0 { + opacity: 0%; + } + .opacity-25 { + opacity: 25%; + } + .opacity-60 { + opacity: 60%; + } + .opacity-75 { + opacity: 75%; + } + .opacity-100 { + opacity: 100%; + } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-xl { + --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-2 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-emerald-500\/10 { + --tw-shadow-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-emerald-500) 10%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-sky-500\/10 { + --tw-shadow-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-sky-500) 10%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-sky-500\/20 { + --tw-shadow-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-sky-500) 20%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-slate-950\/20 { + --tw-shadow-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 20%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .ring-slate-400 { + --tw-ring-color: var(--color-slate-400); + } + .blur { + --tw-blur: blur(8px); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur { + --tw-backdrop-blur: blur(8px); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .backdrop-blur-md { + --tw-backdrop-blur: blur(var(--blur-md)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .backdrop-blur-sm { + --tw-backdrop-blur: blur(var(--blur-sm)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .backdrop-blur-xl { + --tw-backdrop-blur: blur(var(--blur-xl)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-150 { + --tw-duration: 150ms; + transition-duration: 150ms; + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .select-none { + -webkit-user-select: none; + user-select: none; + } + .select-text { + -webkit-user-select: text; + user-select: text; + } + .group-hover\:scale-105 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-scale-x: 105%; + --tw-scale-y: 105%; + --tw-scale-z: 105%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + } + .group-hover\:from-white { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-gradient-from: var(--color-white); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + } + .group-hover\:to-slate-200 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-gradient-to: var(--color-slate-200); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + } + } + .group-hover\:text-accent-foreground { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + .group-hover\:text-accent-foreground\/70 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: color-mix(in srgb, hsl(var(--accent-foreground)) 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-accent-foreground) 70%, transparent); + } + } + } + } + .group-hover\:opacity-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } + .peer-checked\:border-indigo-500 { + &:is(:where(.peer):checked ~ *) { + border-color: var(--color-indigo-500); + } + } + .peer-checked\:bg-indigo-600 { + &:is(:where(.peer):checked ~ *) { + background-color: var(--color-indigo-600); + } + } + .peer-checked\:text-slate-200 { + &:is(:where(.peer):checked ~ *) { + color: var(--color-slate-200); + } + } + .selection\:bg-sky-500 { + & *::selection { + background-color: var(--color-sky-500); + } + &::selection { + background-color: var(--color-sky-500); + } + } + .selection\:text-white { + & *::selection { + color: var(--color-white); + } + &::selection { + color: var(--color-white); + } + } + .after\:absolute { + &::after { + content: var(--tw-content); + position: absolute; + } + } + .after\:top-\[2px\] { + &::after { + content: var(--tw-content); + top: 2px; + } + } + .after\:left-\[2px\] { + &::after { + content: var(--tw-content); + left: 2px; + } + } + .after\:h-\[14px\] { + &::after { + content: var(--tw-content); + height: 14px; + } + } + .after\:w-\[14px\] { + &::after { + content: var(--tw-content); + width: 14px; + } + } + .after\:rounded-full { + &::after { + content: var(--tw-content); + border-radius: calc(infinity * 1px); + } + } + .after\:bg-slate-400 { + &::after { + content: var(--tw-content); + background-color: var(--color-slate-400); + } + } + .after\:transition-all { + &::after { + content: var(--tw-content); + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + } + .after\:content-\[\'\'\] { + &::after { + --tw-content: ''; + content: var(--tw-content); + } + } + .peer-checked\:after\:translate-x-4 { + &:is(:where(.peer):checked ~ *) { + &::after { + content: var(--tw-content); + --tw-translate-x: calc(var(--spacing) * 4); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + } + .peer-checked\:after\:bg-white { + &:is(:where(.peer):checked ~ *) { + &::after { + content: var(--tw-content); + background-color: var(--color-white); + } + } + } + .hover\:scale-\[1\.02\] { + &:hover { + @media (hover: hover) { + scale: 1.02; + } + } + } + .hover\:border-slate-700 { + &:hover { + @media (hover: hover) { + border-color: var(--color-slate-700); + } + } + } + .hover\:border-slate-800 { + &:hover { + @media (hover: hover) { + border-color: var(--color-slate-800); + } + } + } + .hover\:bg-\[\#1e293b\]\/40 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #1e293b 40%, transparent); + } + } + } + .hover\:bg-accent { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + } + .hover\:bg-emerald-500\/20 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); + } + } + } + } + .hover\:bg-indigo-500 { + &:hover { + @media (hover: hover) { + background-color: var(--color-indigo-500); + } + } + } + .hover\:bg-indigo-600 { + &:hover { + @media (hover: hover) { + background-color: var(--color-indigo-600); + } + } + } + .hover\:bg-rose-500\/20 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(64.5% 0.246 16.439) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-rose-500) 20%, transparent); + } + } + } + } + .hover\:bg-rose-950\/20 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(27.1% 0.105 12.094) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-rose-950) 20%, transparent); + } + } + } + } + .hover\:bg-secondary { + &:hover { + @media (hover: hover) { + background-color: var(--color-secondary); + } + } + } + .hover\:bg-secondary\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, hsl(var(--secondary)) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-secondary) 50%, transparent); + } + } + } + } + .hover\:bg-secondary\/80 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, hsl(var(--secondary)) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-secondary) 80%, transparent); + } + } + } + } + .hover\:bg-sky-500\/20 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-sky-500) 20%, transparent); + } + } + } + } + .hover\:bg-slate-900 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-900); + } + } + } + .hover\:bg-slate-900\/60 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 60%, transparent); + } + } + } + } + .hover\:text-accent-foreground { + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + .hover\:text-muted-foreground { + &:hover { + @media (hover: hover) { + color: var(--color-muted-foreground); + } + } + } + .hover\:text-rose-300 { + &:hover { + @media (hover: hover) { + color: var(--color-rose-300); + } + } + } + .hover\:text-slate-200 { + &:hover { + @media (hover: hover) { + color: var(--color-slate-200); + } + } + } + .hover\:text-white { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } + .hover\:underline { + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } + } + .hover\:opacity-90 { + &:hover { + @media (hover: hover) { + opacity: 90%; + } + } + } + .hover\:opacity-95 { + &:hover { + @media (hover: hover) { + opacity: 95%; + } + } + } + .hover\:shadow-sky-500\/20 { + &:hover { + @media (hover: hover) { + --tw-shadow-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-sky-500) 20%, transparent) var(--tw-shadow-alpha), transparent); + } + } + } + } + .focus\:border-emerald-500 { + &:focus { + border-color: var(--color-emerald-500); + } + } + .focus\:border-sky-500 { + &:focus { + border-color: var(--color-sky-500); + } + } + .focus\:bg-accent { + &:focus { + background-color: var(--color-accent); + } + } + .focus\:text-accent-foreground { + &:focus { + color: var(--color-accent-foreground); + } + } + .focus\:ring-2 { + &:focus { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus\:ring-emerald-500 { + &:focus { + --tw-ring-color: var(--color-emerald-500); + } + } + .focus\:ring-rose-500 { + &:focus { + --tw-ring-color: var(--color-rose-500); + } + } + .focus\:ring-sky-500 { + &:focus { + --tw-ring-color: var(--color-sky-500); + } + } + .focus\:ring-offset-2 { + &:focus { + --tw-ring-offset-width: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + } + } + .focus\:ring-offset-\[\#0f172a\] { + &:focus { + --tw-ring-offset-color: #0f172a; + } + } + .focus\:ring-offset-background { + &:focus { + --tw-ring-offset-color: var(--color-background); + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .focus-visible\:ring-2 { + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus-visible\:ring-sky-500\/50 { + &:focus-visible { + --tw-ring-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-sky-500) 50%, transparent); + } + } + } + .focus-visible\:outline-none { + &:focus-visible { + --tw-outline-style: none; + outline-style: none; + } + } + .active\:scale-95 { + &:active { + --tw-scale-x: 95%; + --tw-scale-y: 95%; + --tw-scale-z: 95%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + .sm\:left-0 { + @media (width >= 40rem) { + left: calc(var(--spacing) * 0); + } + } + .sm\:inline { + @media (width >= 40rem) { + display: inline; + } + } + .sm\:grid-cols-2 { + @media (width >= 40rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:px-6 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + } + .sm\:text-6xl { + @media (width >= 40rem) { + font-size: var(--text-6xl); + line-height: var(--tw-leading, var(--text-6xl--line-height)); + } + } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .md\:grid-cols-3 { + @media (width >= 48rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } + .md\:items-center { + @media (width >= 48rem) { + align-items: center; + } + } + .md\:justify-between { + @media (width >= 48rem) { + justify-content: space-between; + } + } + .md\:opacity-0 { + @media (width >= 48rem) { + opacity: 0%; + } + } + .lg\:col-span-1 { + @media (width >= 64rem) { + grid-column: span 1 / span 1; + } + } + .lg\:col-span-2 { + @media (width >= 64rem) { + grid-column: span 2 / span 2; + } + } + .lg\:w-64 { + @media (width >= 64rem) { + width: calc(var(--spacing) * 64); + } + } + .lg\:grid-cols-3 { + @media (width >= 64rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .lg\:flex-row { + @media (width >= 64rem) { + flex-direction: row; + } + } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .peer-checked\:\[\&_svg\]\:opacity-100 { + &:is(:where(.peer):checked ~ *) { + & svg { + opacity: 100%; + } + } + } +} +html, body { + color-scheme: dark; +} +:root { + color-scheme: dark; + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --radius: 0.5rem; +} +* { + scrollbar-width: thin; + scrollbar-color: hsl(var(--border)) transparent; +} +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-button { + display: none; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: hsl(var(--border)); + border-radius: 9999px; + border: 1px solid transparent; + background-clip: padding-box; + transition: background-color 0.2s ease; +} +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground)); +} +input[type="date"], input[type="time"], input[type="datetime-local"] { + position: relative; + padding-left: 0.5rem !important; + padding-right: 1.5rem !important; + color-scheme: dark; + background-color: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + color: hsl(var(--foreground)); + font-family: inherit; +} +::-webkit-datetime-edit { + padding: 0 !important; + flex-shrink: 0 !important; +} +::-webkit-datetime-edit-fields-wrapper { + padding: 0 !important; + flex-shrink: 0 !important; +} +::-webkit-calendar-picker-indicator { + position: absolute; + right: 0.4rem; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease; +} +::-webkit-calendar-picker-indicator:hover { + opacity: 1; +} +::-webkit-datetime-edit-text { + color: hsl(var(--muted-foreground)); + padding: 0 0.125rem; +} +::-webkit-datetime-edit-month-field:focus, ::-webkit-datetime-edit-day-field:focus, ::-webkit-datetime-edit-year-field:focus { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + outline: none; + border-radius: 2px; +} +input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type=number] { + -moz-appearance: textfield; +} +select { + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='hsl(215.4, 16.3%, 56.9%)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 1rem; + padding-right: 2.5rem; +} +.scrollbar-none { + scrollbar-width: none; +} +.scrollbar-none::-webkit-scrollbar { + display: none; +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-gradient-position { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes pulse { + 50% { + opacity: 0.5; + } +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; + --tw-divide-y-reverse: 0; + --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-duration: initial; + --tw-ease: initial; + --tw-content: ""; + } + } +} diff --git a/templates/auth/login.html b/templates/auth/login.html new file mode 100644 index 0000000..b57a634 --- /dev/null +++ b/templates/auth/login.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block title %}Sign In - Stick{% endblock %} + +{% block content %} +
+
+
+ +
+

Welcome Back

+

Sign in to manage your tasks

+
+ + {% if let Some(err) = error %} +
+ + + + {{ err }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ Don't have an account? + Sign up now +
+
+
+{% endblock %} diff --git a/templates/auth/register.html b/templates/auth/register.html new file mode 100644 index 0000000..8c8082e --- /dev/null +++ b/templates/auth/register.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}Sign Up - Stick{% endblock %} + +{% block content %} +
+
+
+ +
+

Create Account

+

Join us to start planning your tasks

+
+ + {% if let Some(err) = error %} +
+ + + + {{ err }} +
+ {% endif %} + + {% if let Some(msg) = success %} +
+ + + + {{ msg }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ Already have an account? + Log in here +
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..7363b5b --- /dev/null +++ b/templates/base.html @@ -0,0 +1,97 @@ + + + + + + + {% block title %}Stick Template{% endblock %} + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + + +
+
+
+ + +
+ {% block content %}{% endblock %} +
+ + +
+
+

© 2026 Stick Template. Built with Axum, Askama, and MongoDB. Styled with Tailwind CSS.

+
+
+ + + diff --git a/templates/components/buttons.html b/templates/components/buttons.html new file mode 100644 index 0000000..6dc76a8 --- /dev/null +++ b/templates/components/buttons.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} + +{% block title %}Buttons - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Actions / Navigation +

Buttons

+

+ Standard button variants including primary, secondary, outlines, and statuses, completed with focus ring outlines, scale transformations on hover, and loading spinner designs. +

+
+ + +
+

Button Variants & Interactive Demos

+ +
+ +
+ + +
+ + +
+ + + + + + + + + + + + + +
+ + + +
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/combobox.html b/templates/components/combobox.html new file mode 100644 index 0000000..ac77bb1 --- /dev/null +++ b/templates/components/combobox.html @@ -0,0 +1,216 @@ +{% extends "base.html" %} + +{% block title %}Autocomplete (Combobox) - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Forms & Inputs +

Autocomplete (Combobox)

+

+ Asynchronous search dropdown inputs driven by HTMX requests with fully integrated keyboard navigation, focus overlays, selection, and hidden inputs for form validation. +

+
+ + +
+

Autocomplete Showcase & Integration

+ +
+ +
+ + + +
+ + +
+ + +
+ Client-Side Filtering +
+ + + + + +
+
+ + +
+ Server-Side HTMX Search + + {% if authenticated %} +
+ + + + +
+ {% else %} +
+
+ + Authentication Required +
+

+ To query the MongoDB database on the server, you must sign in to an active session first. +

+ + Log in to query database + + +
+ {% endif %} +
+
+ + + + + + +
+
+ + +
+

How the Global Combobox JS Works

+

+ The global script combobox.js runs automatically on page load and hooks onto the class names listed below. Ensure your markup uses these selectors to integrate keyboard selection and local/remote filtering: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
.autocomplete-comboboxClass + Wraps all input elements, hidden inputs, and search result list containers. +
.combobox-valueClass + Applied to the <input type="hidden"> element that holds the final selection key (e.g. database ID) for form validation and submission. +
.combobox-inputClass + Applied to the visible <input type="text"> element. Captures typing, trigger clicks, and keyboard arrow navigation events. +
.combobox-resultsClass + The overlay list container. Starts with the class hidden. Toggled open on focus, click, or when query results return. +
.combobox-itemClass + Applied to option elements. Must contain tabindex="0" (for keyboard focus), data-id="..." (database value), and data-name="..." (display value). +
+
+
+ 💡 What is customizable? +

+ You can customize the visual appearance of the options list .combobox-results, option heights, text alignment, input styles, borders, icons, and colors. The functional classes (like .combobox-input, .combobox-value, .combobox-item) must remain intact, and each item must include the data-id, data-name, and tabindex="0" attributes so they participate in keyboard selection cycling and updates. +

+
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/date_time.html b/templates/components/date_time.html new file mode 100644 index 0000000..ddc6d0d --- /dev/null +++ b/templates/components/date_time.html @@ -0,0 +1,392 @@ +{% extends "base.html" %} + +{% block title %}Date & Time Pickers - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Pickers +

Date & Time Pickers

+

+ Custom popup calendars and hour/minute scroll menus constructed using DOM components to prevent relying on native browser-system calendar windows. Designed for optimal styling consistency. +

+
+ + +
+

Date & Time Popovers

+ +
+ +
+ + +
+ + +
+ +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+
+ + + +
+
+ + +
+
+

How the Global Date Picker JS Works

+

+ The custom date picker relies on the following classes and datasets to manage month navigation and update values: +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
.custom-datepickerClass + Wraps the date picker component. Must declare data-year (e.g. 2026) and data-month (0-indexed, 0 = Jan, 4 = May) to initialize the viewport. +
.datepicker-valueClass + Hidden input field (<input type="hidden">) storing the ISO date value (YYYY-MM-DD) for form submission. +
.datepicker-triggerClass + Button clicked by user to open or close the calendar popover dropdown. +
.datepicker-textClass + Target text element updated dynamically to display the formatted selected date (e.g., "May 30, 2026"). +
.datepicker-popoverClass + Floating container containing the month navigation buttons and days grids. Starts as hidden. +
.datepicker-prev / .datepicker-nextClass + Navigational buttons to decrement or increment the active month. +
.datepicker-month-yearClass + Display title label updated dynamically with current month and year (e.g. "May 2026"). +
.datepicker-daysClass + Calendar day grid container dynamically populated with clickable day buttons by the JS engine. +
+
+ +
+

How the Global Time Picker JS Works

+

+ The custom time picker scopes dynamic lists of hours and minutes inside scrollable blocks using the following bindings: +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
.custom-timepickerClass + Wraps the time picker markup structure. +
.timepicker-valueClass + Hidden input field storing the active text time (e.g. "12:00 PM") for forms. +
.timepicker-triggerClass + Clickable button to open the scroll dropdown. +
.timepicker-textClass + Text node inside trigger displaying the formatted time. +
.timepicker-popoverClass + Dropdown overlay panel displaying columns for hour list, minute list, and AM/PM buttons. +
.timepicker-col-hours / .timepicker-col-minutesClass + Containers populated automatically with hourly buttons (1-12) and minute buttons (00-55). +
.timepicker-ampm-btnClass + Buttons representing "AM" and "PM" choices toggled on click. +
+
+ +
+ 💡 What is customizable? +

+ You can customize the styling of the trigger buttons, chevrons, icons, popover cards, borders, shadows, backgrounds, and the active option buttons (which receive .bg-indigo-600). Ensure you preserve the classes and attributes listed above so the DOM-generation functions and trigger listeners in components.js execute without error. +

+
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/feedback.html b/templates/components/feedback.html new file mode 100644 index 0000000..fd6eb0c --- /dev/null +++ b/templates/components/feedback.html @@ -0,0 +1,130 @@ +{% extends "base.html" %} + +{% block title %}Toasts & Alerts - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Feedback +

Toasts & Alerts

+

+ Dynamic toast stack notifications triggered from JavaScript, combined with static SVG alert boxes for validation errors or warnings. +

+
+ + +
+

Interactive Toast & Alert Showcase

+ +
+ +
+ + +
+ + +
+ +
+ Toast Notifications +
+ +
+
+ + +
+ Static Callout Banners + + +
+ + + +
+ General Information + Please remember to assign the task to an active workspace developer. +
+
+ + +
+ + + +
+ Database Sync Issues + Local connections to MongoDB might be interrupted temporarily. +
+
+
+
+ + + +
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/index.html b/templates/components/index.html new file mode 100644 index 0000000..024b349 --- /dev/null +++ b/templates/components/index.html @@ -0,0 +1,133 @@ +{% extends "base.html" %} + +{% block title %}Design System Wiki - Stick{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ + Stick Design System Wiki + +

Component Reference Manual

+

+ Welcome to the Stick design system Wiki. This documentation serves as a living blueprint detailing HTML structures, Tailwind CSS custom variables, and JavaScript trigger hooks required to build premium, high-fidelity interactive elements using the Shadcn aesthetic. +

+
+ + +
+
+
+ + + +
+

Vertical Feature Architecture

+

+ Components are packaged inside feature directories (e.g. src/components/) rather than spread horizontally. Handlers render Askama templates, static JS, and compiled Tailwind assets dynamically. +

+
+ +
+
+ + + +
+

Lightweight & Dependency-Free

+

+ Designed to minimize heavy JS bundles. Using vanilla JavaScript with document-level event delegation (e.g. for Modals, Sheets, Accordions, and Tabs) keeping the interactive shell fast and responsive. +

+
+
+ + +
+

Global HSL Tokens

+

+ The layout relies on standard Tailwind themes mapped onto raw HSL variables, allowing instant utility customization. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRaw HSL MappingRender Example
--background224 71% 4% +
+
--primary210 40% 98% +
+
--border / --input216 34% 17% +
+
--ring216 12.2% 83.9% +
+
+
+
+ + +
+

Understanding the JS Binding Architecture

+

+ Rather than attaching active event listeners to every individual DOM node, the design system utilizes document-level event delegation and global query selector hooks. This keeps page loads incredibly fast and handles dynamically rendered components automatically. +

+ +
+
+ 🔗 The Core Binding Contract +

+ To make components interactive, they must contain specific functional CSS classes or attributes (e.g., .modal-dialog, [data-modal-target], .custom-select) that the Javascript file uses to query the DOM. If these functional hooks are missing or renamed, the interactivity will fail. +

+
+ +
+ 🎨 What is Customizable vs. Fixed? +
    +
  • + Fixed (Functional) Hooks: Standard semantic class names (like .modal-dialog, .modal-content, .modal-backdrop, .modal-close, .select-trigger, and .select-item) are required structure hooks. These must remain unchanged because our JavaScript code expects them to animate opacity, visibility, translation transform stages, and handle keydown events. +
  • +
  • + Customizable (Styling) Rules: Any Tailwind visual classes (like colors e.g. bg-slate-900, padding/margin p-6 mt-2, border-radius rounded-3xl, drop shadows shadow-xl, borders border-border, and custom fonts) can be modified, replaced, or completely restyled. As long as you maintain the core HTML nesting hierarchy and functional selector names, the component will work perfectly. +
  • +
+
+
+
+ +
+
+{% endblock %} diff --git a/templates/components/inputs.html b/templates/components/inputs.html new file mode 100644 index 0000000..0b26746 --- /dev/null +++ b/templates/components/inputs.html @@ -0,0 +1,221 @@ +{% extends "base.html" %} + +{% block title %}Form Fields & Select - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Forms & Inputs +

Form Fields & Select

+

+ Standard text inputs, passwords, multi-line textareas, and custom styled select dropdown elements overdrawn by vector SVG chevron arrows. +

+
+ + +
+

Form Inputs & Select Menu Showcase

+ +
+ +
+ + +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + + +
+
+
+ + + +
+
+ + +
+

How the Global Custom Select JS Works

+

+ The global script components.js monitors specific class names to run the premium custom select elements. Ensure your markup uses these selectors to integrate selection, triggers, and keyboard arrow controls: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
.custom-selectClass + Outer wrapper for the entire select component. Scopes and isolates input values and popovers. +
.select-valueClass + Applied to the <input type="hidden"> that stores the raw value submitted to forms. +
.select-triggerClass + The visible button that user clicks. Toggles popover visibility and chevron rotation on click. +
.select-textClass + Inner text element inside trigger button that dynamically changes its text content to match the selected option. +
.select-chevronClass + Arrow SVG icon. Rotates 180 degrees (adds rotate-180) when the menu opens. +
.select-popoverClass + Floating list container. Starts with the class hidden. Positioned absolutely. +
.select-itemClass + Applied to choices inside popover (usually buttons). Must have data-value="..." containing the raw option value. +
+
+
+ 💡 What is customizable? +

+ You can customize the button trigger layout (paddings, chevrons, fonts, sizing), borders, shadows, options listing alignment, and checkmark icons. Ensure you preserve the classes .custom-select, .select-value, .select-trigger, .select-popover, and .select-item with its data-value attribute so that mouse selections and keyboard arrows function correctly. +

+
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/modals.html b/templates/components/modals.html new file mode 100644 index 0000000..2dd0419 --- /dev/null +++ b/templates/components/modals.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block title %}Dialog Modals - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Overlays +

Dialog Modals

+

+ Overlay window popups centering inside the viewport with scale animations, hardware blur filters, and document-level listeners for escape-key/backdrop dismissals. +

+
+ + +
+

Modal Showcase & Integration

+ +
+ +
+ + +
+ + + + + + +
+
+ + +
+

How the Global Modal JS Works

+

+ The global script components.js runs automatically on page load and monitors specific attributes and classes. Here is what makes the modal operational: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
[data-modal-target]Attribute + Applied to buttons or triggers. The value must match the ID of the modal dialog (e.g., data-modal-target="my-modal"). Clicking it triggers openModal("my-modal"). +
.modal-dialogClass + The outer wrapper container. Starts with the class hidden. The JS removes/adds hidden to show/hide the popup. +
.modal-contentClass + The inner dialog panel card. The JS looks for this class to toggle transitions (adds scale-100 opacity-100 on show; resets to scale-95 opacity-0 on hide). +
.modal-backdropClass + The dark blur overlay. The JS toggles its opacity and registers clicks on it to automatically close the modal. +
.modal-closeClass + Applied to any close button (e.g., "Cancel", "Confirm", or an "X" icon). Clicking any element containing this class triggers modal closure. +
+
+
+ 💡 What is customizable? +

+ You can customize the styling, colors, layout, and sizing of the .modal-content card freely. You must preserve the classes listed above so the Javascript code can target them and apply animations correctly. +

+
+
+ +
+
+ + + + + +{% endblock %} diff --git a/templates/components/scrollbars.html b/templates/components/scrollbars.html new file mode 100644 index 0000000..2700974 --- /dev/null +++ b/templates/components/scrollbars.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block title %}Custom Scrollbars - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Styles +

Custom Scrollbars

+

+ Thin custom scrollbars replacing chunky default native OS scrollbars using modern cross-browser CSS rules. +

+
+ + +
+

Scrollbar Customization Showcase

+ +
+ +
+ + +
+ + +
+
+
Scroll item 1
+
Scroll item 2
+
Scroll item 3
+
Scroll item 4
+
Scroll item 5
+
+
+ + + +
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/sheets.html b/templates/components/sheets.html new file mode 100644 index 0000000..2bf2661 --- /dev/null +++ b/templates/components/sheets.html @@ -0,0 +1,180 @@ +{% extends "base.html" %} + +{% block title %}Slide-over Drawers (Sheets) - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Overlays +

Slide-over Drawers (Sheets)

+

+ Right-anchored slide-out sheets designed for detail inspections, metadata lists, settings panels, and form workflows. +

+
+ + +
+

Drawer Showcase & Integration

+ +
+ +
+ + +
+ + +
+ +
+ + + +
+
+ + +
+

How the Global Sheet JS Works

+

+ The global script components.js monitors specific attributes and classes on page load. Here is the operational contract for sheets: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
[data-sheet-target]Attribute + Applied to buttons or triggers. The value must match the ID of the sheet container (e.g., data-sheet-target="my-sheet"). Clicking it triggers openSheet("my-sheet"). +
.sheet-dialogClass + The outer wrapper sheet container. Starts with the class hidden. Toggled by the script. +
.sheet-contentClass + The inner slide-over panel card. The JS toggles transition translation classes (removes translate-x-full and adds translate-x-0 on show). +
.sheet-backdropClass + The backdrop overlay. The JS toggles its opacity and registers clicks on it to automatically close the sheet. +
.sheet-closeClass + Applied to close trigger buttons. Clicking any element with this class closes the sheet. +
+
+
+ 💡 What is customizable? +

+ You can freely style the placement, width, colors, borders, and contents of the .sheet-content slide-out panel. The functional slide classes (translate-x-full and translate-x-0) and semantic structure class names must be preserved so the script can locate and slide the sheets dynamically. +

+
+
+ +
+
+ + + + + +{% endblock %} diff --git a/templates/components/sidebar.html b/templates/components/sidebar.html new file mode 100644 index 0000000..3865289 --- /dev/null +++ b/templates/components/sidebar.html @@ -0,0 +1,27 @@ + diff --git a/templates/components/tabs_accordion.html b/templates/components/tabs_accordion.html new file mode 100644 index 0000000..b5f1310 --- /dev/null +++ b/templates/components/tabs_accordion.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}Tabs & Accordions - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Layout & Navigation +

Tabs & Accordions

+

+ Horizontal tab groups and collapsible vertical headers with animated rotation chevrons, powered by lightweight document-level event delegation. +

+
+ + +
+

Interactive Navigation Elements

+ +
+ +
+ + +
+ + +
+ +
+ Switch Tabs +
+ + +
+
+
+ Overview parameters content pane. You can place statistics, graphs, or summary tables here. +
+ +
+
+ + +
+ Accordion Collapsible +
+
+ + +
+
+
+
+ + + +
+
+ + +
+
+

How the Global Tabs JS Works

+

+ The global script components.js handles click switches for tabs via specific dataset variables. Here is the contract: +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeTypeBehavior & Purpose
data-tab-groupAttribute + Applied to buttons and triggers. Isolates tab groups on the same page. Tab buttons in the same group toggle together (e.g., data-tab-group="group1"). +
data-tab-targetAttribute + Specifies the ID of the content element pane that should be revealed when this tab is clicked (e.g., data-tab-target="my-pane-1"). +
data-tab-content-groupAttribute + Applied to the content elements. Must match the data-tab-group string. Click triggers hide all content elements in this group and show the target. +
+
+ +
+

How the Global Accordion JS Works

+

+ Accordions are toggled through click delegation looking for the following class tree: +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Selector / ClassTypeBehavior & Purpose
.accordion-itemClass + Surrounds the single collapsible item container (trigger header + content block). +
.accordion-triggerClass + Clickable header button. Toggles display classes on the sibling content element. +
.accordion-contentClass + Contained collapsible item body text. Starts with the class hidden. Toggled by the script. +
.accordion-chevronClass + Optional vector arrow icon inside trigger. The JS applies rotate-180 animation classes on show. +
+
+
+ 💡 What is customizable? +

+ For Tabs, you can style the tab list buttons (direction, active borders, colors) and content layout freely. Just ensure data-tab-group matches between triggers and active panes, and data-tab-target matches the pane ID. + For Accordions, you can design the headers, chevron SVGs, background panels, and borders. You must maintain the .accordion-item, .accordion-trigger, and .accordion-content class selectors so the script can toggle the collapsed state. +

+
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/toggles.html b/templates/components/toggles.html new file mode 100644 index 0000000..3021332 --- /dev/null +++ b/templates/components/toggles.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} + +{% block title %}Switches & Checkboxes - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Toggles +

Switches & Checkboxes

+

+ Premium animated toggle elements, range sliders, and custom styled checkboxes that mask standard hidden inputs with custom animations. +

+
+ + +
+

Interactive Toggles & Checkbox Demos

+ +
+ +
+ + +
+ + +
+ +
+ Toggle Status + +
+ + +
+ +
+ + +
+ +
+ + 50% +
+
+
+ + + +
+
+ +
+
+ + +{% endblock %} diff --git a/templates/components/visuals.html b/templates/components/visuals.html new file mode 100644 index 0000000..0ad7d9b --- /dev/null +++ b/templates/components/visuals.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} + +{% block title %}Avatars & Badges - Design System Wiki{% endblock %} + +{% block content %} +
+ + + {% include "components/sidebar.html" %} + + +
+ + +
+ Visuals +

Avatars & Badges

+

+ Circular user avatars featuring fallback initials text, combined with status badges in sky, emerald, amber, and rose variants. +

+
+ + +
+

Avatars & Badges Showcase

+ +
+ +
+ + +
+ + +
+ +
+ Fallback Avatars +
+
AV
+
BC
+
JD
+ Textual initials fallback inside boundaries +
+
+ + +
+ Status Badges +
+ In Progress + Completed + Reviewing + Blocked +
+
+
+ + + +
+
+ +
+
+ + +{% endblock %} diff --git a/templates/developers/edit.html b/templates/developers/edit.html new file mode 100644 index 0000000..242f507 --- /dev/null +++ b/templates/developers/edit.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}Edit Developer - Stick{% endblock %} + +{% block content %} +
+
+
+ +
+

Edit Developer

+

Update the developer's profile and skills

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancel + + +
+
+
+
+{% endblock %} diff --git a/templates/developers/list.html b/templates/developers/list.html new file mode 100644 index 0000000..93f2fd9 --- /dev/null +++ b/templates/developers/list.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}Developers - Stick{% endblock %} + +{% block content %} +
+ + +
+
+

Developers

+

Manage developers to assign them workflow tasks

+
+
+ + Total Developers: {{ developers.len() }} + +
+
+ +
+ + +
+
+

+ + + + Add Developer +

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + +
+ {% if developers.is_empty() %} +
+ + + +

No developers added

+

Add developers using the form on the left to start assigning tasks.

+
+ {% else %} + {% for dev in developers %} +
+
+

{{ dev.name }}

+

+ + + + {{ dev.email }} +

+ + + {% if !dev.skills.is_empty() %} +
+ {% for skill in dev.skills %} + + {{ skill }} + + {% endfor %} +
+ {% endif %} +
+ + +
+ + + + + +
+ +
+
+
+ {% endfor %} + {% endif %} +
+
+
+{% endblock %} diff --git a/templates/developers/search_results.html b/templates/developers/search_results.html new file mode 100644 index 0000000..f282392 --- /dev/null +++ b/templates/developers/search_results.html @@ -0,0 +1,15 @@ +{% if developers.is_empty() %} +
No developers found matching this query
+{% else %} +
+ {% for dev in developers %} +
+ {{ dev.name }} + Select +
+ {% endfor %} +
+{% endif %} diff --git a/templates/main_view/index.html b/templates/main_view/index.html new file mode 100644 index 0000000..24c51d6 --- /dev/null +++ b/templates/main_view/index.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} + +{% block title %}Stick - Use-Case Oriented Rust Web Template{% endblock %} + +{% block content %} +
+
+ + + + Axum 0.8 + MongoDB v3 + Askama 0.16 + + + +

+ Clean, Use-Case Centric + Rust Web Development +

+ + +

+ A production-ready template organized around vertical features. Auth, databases, and domain logic are grouped together by use-case for high maintainability. +

+ + +
+ {% if authenticated %} + + Go to Tasks Dashboard + + {% else %} + + Get Started Free + + + Sign In + + {% endif %} +
+ + +
+
+
+ + + +
+

Type-Safe Views

+

Askama renders templates compiled directly into Rust code, catching missing arguments or syntax bugs at compile time.

+
+ +
+
+ + + +
+

Secure Authentication

+

Built-in JWT session verification, cookie storage, bcrypt password hashing, and custom Axum 0.8 guard extractors.

+
+ +
+
+ + + +
+

MongoDB Integration

+

Seamless BSON v3 mapping, index setups, and type-safe `chrono::DateTime` conversion via `serde_with` serialize helpers.

+
+
+
+
+{% endblock %} diff --git a/templates/tasks/dashboard.html b/templates/tasks/dashboard.html new file mode 100644 index 0000000..a443de2 --- /dev/null +++ b/templates/tasks/dashboard.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block title %}Tasks Dashboard - Stick{% endblock %} + +{% block content %} +
+ + +
+
+

Your Tasks

+

Add and manage your workflow tasks

+
+
+ + Total Tasks: {{ tasks.len() }} + +
+
+ +
+ + +
+
+

+ + + + New Task +

+
+
+ + +
+ +
+ + +
+ + +
+ + + + + +
+ + + +
+ + + + +
+
+ + + +
+ + +
+
+
+ + +
+ {% if tasks.is_empty() %} +
+ + + +

No tasks yet

+

Your task board is clean. Use the form on the left to add your first task.

+
+ {% else %} + {% for item in tasks %} +
+ + +
+ {% if item.task.is_completed %} +
+ + + +
+ {% else %} +
+ {% endif %} +
+ + +
+

+ {{ item.task.title }} +

+ {% if let Some(desc) = item.task.description %} + {% if !desc.is_empty() %} +

+ {{ desc }} +

+ {% endif %} + {% endif %} + +
+ Created {{ item.task.created_at.format("%Y-%m-%d %H:%M") }} + {% if let Some(dev_name) = item.developer_name %} + + + + + + Assigned to: {{ dev_name }} + + {% endif %} +
+
+ + +
+ {% if !item.task.is_completed %} +
+ +
+ {% endif %} +
+ +
+
+
+ {% endfor %} + {% endif %} +
+
+
+ +{% endblock %}