commit f42a5f05b2598781a4dcfc7b77108e1f8f7cc6bd Author: Enciphered Date: Sat May 30 01:09:14 2026 +0500 feat: initialize template shell and basic components 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 %}