commit 6bb9a127a051848c7af914bfa302dab59e12b4fe Author: Alexey Date: Thu Apr 23 23:34:39 2026 +0500 mantra v0.1: book-grade reader for design-dna corpus + margin drawer (notes + claude-ask) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11b99a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/content +.DS_Store +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..20b9281 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3529 @@ +# 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", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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 = "any_spawner" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" +dependencies = [ + "futures", + "thiserror 2.0.18", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + +[[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 = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "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 = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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 = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml 1.1.2+spec-1.1.0", + "winnow", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-str" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case_extras" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb" +dependencies = [ + "convert_case 0.11.0", +] + +[[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 = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[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 = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "either_of" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" + +[[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 = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[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 = "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", +] + +[[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", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gray_matter" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8666976c40b8633f918783969b6681a3ddb205f29150348617de425d85a3e3bd" +dependencies = [ + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust2", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +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 = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[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 = "hydration_context" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283" +dependencies = [ + "futures", + "js-sys", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", + "wasm-bindgen", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[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 = "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "leptos" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa3982e7fe36c1de68f91f3c9083124f389a975523881f3d7e3363362feda41" +dependencies = [ + "any_spawner", + "base64", + "cfg-if", + "either_of", + "futures", + "getrandom 0.4.2", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "rand", + "reactive_graph", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.18", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_split_helpers", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ac7734eed700b0170dffbfc93b03491ed1f306622d79625323a21ed0eedac0" +dependencies = [ + "any_spawner", + "axum", + "futures", + "hydration_context", + "leptos", + "leptos_integration_utils", + "leptos_macro", + "leptos_meta", + "leptos_router", + "or_poisoned", + "server_fn", + "tachys", + "tokio", + "tower", + "tower-http", +] + +[[package]] +name = "leptos_config" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c06f751315bccc0d193fab302ac01d25bcfcd97474d4676440e7e3250dc3fc3" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.18", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "or_poisoned", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c097f89cd9aa606297672f56fa5bdda09f01609a9f4eefaccdbb5ab5afea4279" +dependencies = [ + "futures", + "hydration_context", + "leptos", + "leptos_config", + "leptos_meta", + "leptos_router", + "reactive_graph", +] + +[[package]] +name = "leptos_macro" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9360df573fb57582384a8b7640a3de94ce6501d49be3b69f637cf11a42da484b" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.11.0", + "convert_case_extras", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "rustc_version", + "server_fn_macro", + "syn", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c3efe657b4c55ed2e078922786ffe20acfb71767c3dd913767b09a35c75c890" +dependencies = [ + "futures", + "indexmap", + "leptos", + "or_poisoned", + "send_wrapper", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15158449162e099e2273442f7fd9b924f5cefd935d52af5755ec62aa819fa52" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net", + "js-sys", + "leptos", + "leptos_router_macro", + "or_poisoned", + "percent-encoding", + "reactive_graph", + "rustc_version", + "send_wrapper", + "tachys", + "thiserror 2.0.18", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "leptos_server" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mantra-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "gray_matter", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "mantra-ui", + "pulldown-cmark", + "serde", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mantra-ui" +version = "0.1.0" +dependencies = [ + "chrono", + "console_error_panic_hook", + "leptos", + "leptos_meta", + "leptos_router", + "reqwest", + "serde", + "serde_json", + "tokio", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[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 = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[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 = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "reactive_graph" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c5a025366836190c7030e883cc2bcd9e384ff555336e3c7954741ca411b177" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "indexmap", + "or_poisoned", + "paste", + "pin-project-lite", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30fd35b7d299c591293bb69fed47a703eb2703b1cff0493e78b16ed007e5382" +dependencies = [ + "guardian", + "indexmap", + "itertools", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", + "send_wrapper", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d8e790a5ae5ddf9b7fa380c728375b06858e0cca7d063a73b3408320c523e1" +dependencies = [ + "convert_case 0.11.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[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 = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror 2.0.18", +] + +[[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 = "rustls" +version = "0.23.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[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_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.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "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_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "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 = "server_fn" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d60e4c1dfccd91fe0990141f69f1d5cf5679797ad53aa1b45e5bd658eb119f0" +dependencies = [ + "axum", + "base64", + "bytes", + "const-str", + "const_format", + "futures", + "gloo-net", + "http", + "http-body-util", + "hyper", + "inventory", + "js-sys", + "or_poisoned", + "pin-project-lite", + "rustc_version", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.18", + "throw_error", + "tokio", + "tower", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f" +dependencies = [ + "const_format", + "convert_case 0.11.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "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 = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +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 = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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 = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[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 = "tachys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2989c94c59db8497727875aa561d4d0daa3cc79b5774d5ced48263f7091beff1" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "either_of", + "erased", + "futures", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "next_tuple", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "rustc_version", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "throw_error" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" +dependencies = [ + "pin-project-lite", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +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-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[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-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "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 = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", +] + +[[package]] +name = "typed-builder" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" +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 = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +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", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_split_helpers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0cb6d1008be3c4c5abc31a407bfb8c8449ae14efc8561c1db821f79b9614b0a" +dependencies = [ + "async-once-cell", + "wasm_split_macros", +] + +[[package]] +name = "wasm_split_macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a659ffe5c7f4538aa6357c07e3d73221cc61eba03bd9a081e14bc91ed09b8c" +dependencies = [ + "base16", + "quote", + "sha2", + "syn", +] + +[[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", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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 = "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-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 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +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", + "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", + "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", + "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 = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[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.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +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" + +[[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..0fcbd14 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,58 @@ +[workspace] +resolver = "2" +members = ["crates/mantra-server", "crates/mantra-ui"] + +[workspace.dependencies] +leptos = { version = "0.8" } +leptos_axum = "0.8" +leptos_router = { version = "0.8" } +leptos_meta = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +axum = "0.8" +tower-http = { version = "0.6", features = ["trace", "fs"] } +tracing = "0.1" +tracing-subscriber = "0.3" +anyhow = "1" +pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] } +gray_matter = "0.2" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 + +[profile.dev.package.leptos_macro] +opt-level = 3 + +# cargo-leptos workspace config +[[workspace.metadata.leptos]] +name = "mantra" +bin-package = "mantra-server" +bin-exe-name = "mantra-server" +bin-features = ["ssr"] +bin-default-features = false +lib-package = "mantra-ui" +lib-features = ["hydrate"] +lib-default-features = false +output-name = "mantra" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "sass/main.scss" +assets-dir = "static" +site-addr = "127.0.0.1:9920" +reload-port = 3040 +env = "DEV" +browserquery = "defaults" +lib-profile-release = "wasm-release" + +[profile.wasm-release] +inherits = "release" +opt-level = "z" +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = "symbols" diff --git a/crates/mantra-server/Cargo.toml b/crates/mantra-server/Cargo.toml new file mode 100644 index 0000000..e229972 --- /dev/null +++ b/crates/mantra-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "mantra-server" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "mantra-server" +path = "src/main.rs" + +[dependencies] +mantra-ui = { path = "../mantra-ui", features = ["ssr"] } +leptos = { workspace = true, features = ["ssr"] } +leptos_axum = { workspace = true } +leptos_router = { workspace = true, features = ["ssr"] } +leptos_meta = { workspace = true, features = ["ssr"] } +axum = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +anyhow = { workspace = true } +pulldown-cmark = { workspace = true } +gray_matter = { workspace = true } +serde = { workspace = true } + +[features] +default = [] +ssr = [] diff --git a/crates/mantra-server/src/corpus_loader.rs b/crates/mantra-server/src/corpus_loader.rs new file mode 100644 index 0000000..0b35c93 --- /dev/null +++ b/crates/mantra-server/src/corpus_loader.rs @@ -0,0 +1,331 @@ +//! Loads the design-dna corpus from a directory of markdown files. +//! +//! Layout expected (relative to MANTRA_CONTENT_DIR): +//! cycle-1-philosophy/ +//! _index.md ← corpus-level index with themes +//! .md × N ← one distillation per source +//! +//! The loader returns an in-memory `Corpus` ready for SSR renders. + +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use gray_matter::engine::YAML; +use gray_matter::Matter; +use pulldown_cmark::{html, Event, Options, Parser, Tag, TagEnd}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use serde::Deserialize; + +use mantra_ui::corpus::{Corpus, Source, Theme}; + +#[derive(Deserialize, Debug, Default)] +struct SourceFrontmatter { + #[serde(default)] + source: String, + #[serde(default)] + tags: Vec, + #[serde(default)] + confidence: String, + #[serde(default, rename = "type")] + _kind: String, +} + +pub fn load_corpus(root: &Path) -> Result { + load_corpus_alt_cycle_dir(root, "cycle-1-philosophy") +} + +pub fn load_corpus_alt_cycle_dir(root: &Path, cycle_subdir: &str) -> Result { + let cycle_dir = root.join(cycle_subdir); + if !cycle_dir.is_dir() { + return Err(anyhow!( + "content dir missing {} subdir: {}", + cycle_subdir, + cycle_dir.display() + )); + } + + // First pass: gather all source files (exclude _index.md). + let mut source_paths: Vec = Vec::new(); + for entry in fs::read_dir(&cycle_dir)? { + let path = entry?.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + if !name.ends_with(".md") { + continue; + } + if name.starts_with('_') { + continue; + } + source_paths.push(path); + } + source_paths.sort(); + + let mut sources: HashMap = HashMap::new(); + let mut order: Vec = Vec::new(); + for path in &source_paths { + let src = parse_source(path).with_context(|| { + format!("parsing source {}", path.display()) + })?; + order.push(src.slug.clone()); + sources.insert(src.slug.clone(), src); + } + + // Themes come from the _index.md if present; skip gracefully otherwise. + let themes = parse_themes(&cycle_dir.join("_index.md")) + .unwrap_or_else(|e| { + tracing::warn!("no themes loaded: {e}"); + Vec::new() + }); + + Ok(Corpus { order, sources, themes }) +} + +fn parse_source(path: &Path) -> Result { + let raw = fs::read_to_string(path)?; + let matter = Matter::::new(); + let parsed = matter.parse(&raw); + let fm: SourceFrontmatter = parsed + .data + .as_ref() + .and_then(|p| p.deserialize().ok()) + .unwrap_or_default(); + + let body_md = parsed.content; + let slug = path + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow!("bad filename: {}", path.display()))? + .to_string(); + + // Extract H1 (### replaces some files — just grab the first #-line). + let title = body_md + .lines() + .find(|l| l.starts_with("# ")) + .map(|l| l.trim_start_matches("# ").trim().to_string()) + .unwrap_or_else(|| slug.clone()); + + let core_claim = extract_section(&body_md, "## Core claim") + .unwrap_or_default() + .trim() + .replace('\n', " "); + + let author = fm + .source + .split(',') + .next() + .unwrap_or(&slug) + .trim() + .to_string(); + + let body_html = md_to_html(&body_md); + + Ok(Source { + slug, + title, + author, + core_claim, + tags: fm.tags, + confidence: if fm.confidence.is_empty() { + "medium".into() + } else { + fm.confidence + }, + body_html, + }) +} + +/// Extract text content between a given heading and the next `## ` heading. +fn extract_section(md: &str, heading: &str) -> Option { + let mut out = String::new(); + let mut capturing = false; + for line in md.lines() { + if line.trim_start().starts_with(heading) { + capturing = true; + continue; + } + if capturing && line.trim_start().starts_with("## ") { + break; + } + if capturing { + out.push_str(line); + out.push('\n'); + } + } + if out.trim().is_empty() { None } else { Some(out) } +} + +/// Render markdown to HTML and inject stable `data-para-id` attributes +/// on every `

` element. The id is a short content-hash: stable +/// across minor edits of the surrounding text, unique enough within a +/// single source document. The margin-layer UI uses these ids to +/// anchor notes and Claude-asks to specific paragraphs. +fn md_to_html(md: &str) -> String { + let mut opts = Options::empty(); + opts.insert(Options::ENABLE_TABLES); + opts.insert(Options::ENABLE_FOOTNOTES); + opts.insert(Options::ENABLE_STRIKETHROUGH); + opts.insert(Options::ENABLE_TASKLISTS); + opts.insert(Options::ENABLE_SMART_PUNCTUATION); + + // First pass: render plain HTML. + let parser = Parser::new_ext(md, opts); + let mut raw = String::new(); + html::push_html(&mut raw, parser); + + // Second pass: re-parse to capture paragraph text for hashing, and + // inject data-para-id. pulldown-cmark emits events in order, so we + // can walk the source again, track paragraphs, compute hashes, and + // then do a simple textual rewrite on the output. + let parser2 = Parser::new_ext(md, opts); + let mut current_para_text = String::new(); + let mut in_para = false; + let mut para_hashes: Vec = Vec::new(); + for ev in parser2 { + match ev { + Event::Start(Tag::Paragraph) => { + in_para = true; + current_para_text.clear(); + } + Event::End(TagEnd::Paragraph) => { + let id = short_hash(current_para_text.trim()); + para_hashes.push(id); + in_para = false; + } + Event::Text(t) if in_para => current_para_text.push_str(&t), + Event::Code(t) if in_para => current_para_text.push_str(&t), + _ => {} + } + } + + // Inject attributes: replace each `

` (in order) with + // `

`. We walk the byte buffer to find the + // ASCII literal `

`, but copy UTF-8 slices (not byte-by-byte) + // to preserve non-ASCII correctly — the earlier `push(u8 as char)` + // approach silently decoded multibyte UTF-8 as Latin-1 and turned + // em-dashes / smart-quotes / Cyrillic / Greek into mojibake. + let mut out = String::with_capacity(raw.len() + para_hashes.len() * 24); + let mut pending = para_hashes.into_iter(); + let bytes = raw.as_bytes(); + let mut last_copied = 0usize; + let mut i = 0usize; + while i + 3 <= bytes.len() { + if &bytes[i..i + 3] == b"

" { + // flush everything from last_copied..i as a proper str slice + out.push_str(&raw[last_copied..i]); + if let Some(id) = pending.next() { + out.push_str(&format!(r#"

"#)); + } else { + out.push_str("

"); + } + i += 3; + last_copied = i; + } else { + i += 1; + } + } + out.push_str(&raw[last_copied..]); + out +} + +fn short_hash(s: &str) -> String { + let mut h = DefaultHasher::new(); + s.hash(&mut h); + format!("{:08x}", h.finish() & 0xFFFF_FFFF) +} + +/// Parse `_index.md` for the 12 running themes + their contributing sources. +/// +/// Format assumption: each theme is introduced by `### {n}. {title}` +/// with the following paragraph carrying a comma-separated list of +/// author names. We map author names back to slugs by substring-match +/// on the source files' author field. Fallback if pattern doesn't +/// match: empty themes list, landing shows empty themes column. +fn parse_themes(index_path: &Path) -> Result> { + let raw = fs::read_to_string(index_path)?; + let mut themes: Vec = Vec::new(); + let mut current: Option<(String, String, String)> = None; // (slug, title, desc_md) + + for line in raw.lines() { + if let Some(rest) = line.strip_prefix("### ") { + if let Some((slug, title, desc)) = current.take() { + themes.push(finalize_theme(slug, title, desc)); + } + // e.g. "### 1. Emptiness / absence как носитель формы" + let rest = rest.trim(); + let after_num = rest + .split_once('.') + .map(|(_, r)| r.trim().to_string()) + .unwrap_or_else(|| rest.to_string()); + let slug = slugify(&after_num); + current = Some((slug, after_num, String::new())); + } else if line.starts_with("## ") { + // exit "running themes" section — fallthrough to push last theme + if let Some((slug, title, desc)) = current.take() { + themes.push(finalize_theme(slug, title, desc)); + } + } else if let Some((_, _, desc)) = current.as_mut() { + desc.push_str(line); + desc.push('\n'); + } + } + if let Some((slug, title, desc)) = current.take() { + themes.push(finalize_theme(slug, title, desc)); + } + Ok(themes) +} + +fn finalize_theme(slug: String, title: String, desc_md: String) -> Theme { + let contributing = parse_theme_contributors(&desc_md); + let description_html = md_to_html(desc_md.trim()); + Theme { + slug, + title, + description_html, + contributing, + } +} + +/// Very loose extraction: pull parenthesis-grouped author names out of +/// the theme description and attempt to match them to known sources. +/// Since we don't yet have the corpus at this point, we just return a +/// best-guess list of tokens — the landing component can filter by +/// membership later if desired. +fn parse_theme_contributors(desc: &str) -> Vec { + // Heuristic: grab author names from the first parenthetical group. + let Some(start) = desc.find('(') else { + return Vec::new(); + }; + let Some(end) = desc[start..].find(')') else { + return Vec::new(); + }; + let inside = &desc[start + 1..start + end]; + inside + .split([',', '·']) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| { + // Strip parenthetical hints like "Laozi (ch. 11)" → "Laozi" + s.split_whitespace() + .next() + .unwrap_or(s) + .to_string() + }) + .map(|s| slugify(&s)) + .filter(|s| !s.is_empty()) + .collect() +} + +fn slugify(s: &str) -> String { + s.to_lowercase() + .chars() + .map(|c| if c.is_alphanumeric() { c } else { '-' }) + .collect::() + .split('-') + .filter(|p| !p.is_empty()) + .collect::>() + .join("-") +} diff --git a/crates/mantra-server/src/main.rs b/crates/mantra-server/src/main.rs new file mode 100644 index 0000000..c8c3585 --- /dev/null +++ b/crates/mantra-server/src/main.rs @@ -0,0 +1,119 @@ +//! mantra server — axum + leptos_axum SSR. +//! +//! Loads the corpus from disk once at startup (env `MANTRA_CONTENT_DIR`), +//! hands a shared `Arc` to every render pass via Leptos context. + +#![recursion_limit = "256"] + +mod corpus_loader; + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use axum::routing::any; +use axum::Router; +use leptos::config::get_configuration; +use leptos::prelude::provide_context; +use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes}; +use tower_http::services::ServeDir; +use tower_http::trace::TraceLayer; +use tracing::info; + +use mantra_ui::app::{shell, App}; +use mantra_ui::corpus::{BilingualCorpus, BilingualHandle}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_env("MANTRA_LOG") + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .without_time() + .init(); + + let content_dir = PathBuf::from( + std::env::var("MANTRA_CONTENT_DIR") + .unwrap_or_else(|_| "content".to_string()), + ); + info!("loading bilingual corpus from {}", content_dir.display()); + + // The loader expects a language-specific subdir; we call it twice + // against the two sibling directories. + let en = corpus_loader::load_corpus(&content_dir)?; + // ru lives under ../-ru relative path — or env-overridden. + let ru_dir = std::env::var("MANTRA_CONTENT_DIR_RU") + .map(PathBuf::from) + .unwrap_or_else(|_| { + // derive from content_dir by replacing trailing component + let mut p = content_dir.clone(); + let last = p + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| String::from("content")); + p.pop(); + p.push(format!("{last}-ru")); + p + }); + // Hack for symlinked content: our Mac dev uses `content` symlink to + // design-dna/, so content-ru doesn't exist at peer — fall back to the + // sibling `cycle-1-philosophy-ru` under the DESIGN-DNA root instead. + let ru = if ru_dir.is_dir() { + corpus_loader::load_corpus(&ru_dir)? + } else { + // if MANTRA_CONTENT_DIR is design-dna root, then ru lives under + // /cycle-1-philosophy-ru — but that's a file layout + // difference, handled inside load_corpus_ru. + corpus_loader::load_corpus_alt_cycle_dir(&content_dir, "cycle-1-philosophy-ru")? + }; + + let bilingual: BilingualHandle = Arc::new(BilingualCorpus { + ru: Arc::new(ru), + en: Arc::new(en), + }); + info!( + "loaded: ru={} sources / en={} sources, themes={}", + bilingual.ru.sources.len(), + bilingual.en.sources.len(), + bilingual.ru.themes.len() + ); + + let conf = get_configuration(None) + .map_err(|e| anyhow::anyhow!("leptos config: {e}"))?; + let leptos_options = conf.leptos_options; + let addr: SocketAddr = leptos_options.site_addr; + let routes = generate_route_list(App); + + let bilingual_ctx = bilingual.clone(); + let bilingual_for_sfn = bilingual.clone(); + let app = Router::new() + .route( + "/api/{*fn_name}", + any(move |req| { + let ctx = bilingual_for_sfn.clone(); + handle_server_fns_with_context( + move || provide_context(ctx.clone()), + req, + ) + }), + ) + .leptos_routes_with_context( + &leptos_options, + routes, + move || provide_context(bilingual_ctx.clone()), + { + let opts = leptos_options.clone(); + move || shell(opts.clone()) + }, + ) + .fallback_service(ServeDir::new("target/site")) + .layer(TraceLayer::new_for_http()) + .with_state(leptos_options); + + info!("mantra listening on {addr}"); + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app.into_make_service()).await?; + Ok(()) +} diff --git a/crates/mantra-ui/Cargo.toml b/crates/mantra-ui/Cargo.toml new file mode 100644 index 0000000..fa80baa --- /dev/null +++ b/crates/mantra-ui/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "mantra-ui" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +leptos.workspace = true +leptos_router.workspace = true +leptos_meta.workspace = true +serde.workspace = true +chrono = { workspace = true } +console_error_panic_hook = { version = "0.1", optional = true } +wasm-bindgen = { version = "0.2.118", optional = true } +web-sys = { version = "0.3", optional = true, features = [ + "Window", + "Storage", + "Document", + "Element", + "HtmlElement", + "HtmlTextAreaElement", + "HtmlCollection", + "NodeList", + "DomTokenList", + "Event", + "EventTarget", + "console", +] } +# Server-only — pulled in under ssr feature for notes I/O + Claude HTTP. +tokio = { workspace = true, optional = true } +reqwest = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + +[features] +default = [] +hydrate = [ + "leptos/hydrate", + "dep:console_error_panic_hook", + "dep:wasm-bindgen", + "dep:web-sys", +] +ssr = [ + "leptos/ssr", + "leptos_router/ssr", + "leptos_meta/ssr", + "dep:tokio", + "dep:reqwest", + "dep:serde_json", +] diff --git a/crates/mantra-ui/src/api/mod.rs b/crates/mantra-ui/src/api/mod.rs new file mode 100644 index 0000000..766228f --- /dev/null +++ b/crates/mantra-ui/src/api/mod.rs @@ -0,0 +1,460 @@ +//! Server functions for the margin layer: notes and Claude-ask. +//! +//! Notes live as markdown files in `$MANTRA_NOTES_DIR/.md`. +//! Each file is append-oriented: a note is a dated entry anchored to +//! a paragraph-id. Subsequent reads parse the file and return +//! structured entries. + +pub mod types; + +use leptos::prelude::*; + +pub use types::{LandingData, NoteEntry, NoteKind, SourcePageData, ThemePageData}; + +// --- fetch_source_page ------------------------------------------------- +// +// Snapshot for `/source/:slug`. Moves corpus access into a server fn +// so SSR and hydrate render from the same Resource payload and the +// client doesn't need the full `Arc` in context. + +#[server(endpoint = "source_page")] +pub async fn fetch_source_page( + slug: String, + lang: String, +) -> Result, ServerFnError> { + #[cfg(feature = "ssr")] + { + use crate::corpus::{BilingualHandle, Lang}; + let bilingual = use_context::() + .ok_or_else(|| ServerFnError::new("no corpus in context"))?; + let lang = Lang::from_query(Some(&lang)); + let corpus = bilingual.for_lang(lang); + let Some(src) = corpus.get(&slug).cloned() else { + return Ok(None); + }; + let (prev, next) = corpus.neighbors(&slug); + Ok(Some(SourcePageData { + source: src, + prev: prev.map(|s| s.to_string()), + next: next.map(|s| s.to_string()), + })) + } + #[cfg(not(feature = "ssr"))] + { + let _ = (slug, lang); + unreachable!() + } +} + +// --- fetch_landing ----------------------------------------------------- + +#[server(endpoint = "landing")] +pub async fn fetch_landing(lang: String) -> Result { + #[cfg(feature = "ssr")] + { + use crate::corpus::{BilingualHandle, Lang}; + let bilingual = use_context::() + .ok_or_else(|| ServerFnError::new("no corpus in context"))?; + let lang = Lang::from_query(Some(&lang)); + let corpus = bilingual.for_lang(lang); + let sources = corpus + .order + .iter() + .filter_map(|slug| corpus.get(slug).cloned()) + .collect(); + Ok(LandingData { + order: corpus.order.clone(), + sources, + themes: corpus.themes.clone(), + }) + } + #[cfg(not(feature = "ssr"))] + { + let _ = lang; + unreachable!() + } +} + +// --- fetch_theme_page -------------------------------------------------- + +#[server(endpoint = "theme_page")] +pub async fn fetch_theme_page( + slug: String, + lang: String, +) -> Result, ServerFnError> { + #[cfg(feature = "ssr")] + { + use crate::corpus::{BilingualHandle, Lang}; + let bilingual = use_context::() + .ok_or_else(|| ServerFnError::new("no corpus in context"))?; + let lang = Lang::from_query(Some(&lang)); + let corpus = bilingual.for_lang(lang); + let Some(theme) = corpus.themes.iter().find(|t| t.slug == slug).cloned() else { + return Ok(None); + }; + let contributing = theme + .contributing + .iter() + .filter_map(|s| corpus.get(s).cloned()) + .collect(); + Ok(Some(ThemePageData { theme, contributing })) + } + #[cfg(not(feature = "ssr"))] + { + let _ = (slug, lang); + unreachable!() + } +} + +// --- fetch_notes ------------------------------------------------------- + +#[server(endpoint = "notes_fetch")] +pub async fn fetch_notes(source_slug: String) -> Result, ServerFnError> { + #[cfg(feature = "ssr")] + { + let dir = std::env::var("MANTRA_NOTES_DIR").map_err(|_| { + ServerFnError::new("MANTRA_NOTES_DIR not set on server") + })?; + let path = std::path::PathBuf::from(&dir).join(format!("{source_slug}.md")); + if !path.exists() { + return Ok(Vec::new()); + } + let raw = tokio::fs::read_to_string(&path) + .await + .map_err(|e| ServerFnError::new(format!("read notes: {e}")))?; + Ok(parse_marginalia(&raw)) + } + #[cfg(not(feature = "ssr"))] + { + unreachable!() + } +} + +// --- save_note --------------------------------------------------------- + +#[server(endpoint = "notes_save")] +pub async fn save_note( + source_slug: String, + para_id: String, + para_excerpt: String, + note_text: String, + author: String, +) -> Result<(), ServerFnError> { + #[cfg(feature = "ssr")] + { + if note_text.trim().is_empty() { + return Err(ServerFnError::new("note text is empty")); + } + append_marginalia_entry( + &source_slug, + ¶_id, + ¶_excerpt, + MarginaliaPayload::Note(note_text), + &author, + ) + .await?; + spawn_git_autocommit(&source_slug, ¶_id).await; + Ok(()) + } + #[cfg(not(feature = "ssr"))] + { + unreachable!() + } +} + +// --- ask_claude -------------------------------------------------------- + +#[server(endpoint = "claude_ask")] +pub async fn ask_claude( + source_slug: String, + para_id: String, + para_excerpt: String, + question: String, + author: String, +) -> Result { + #[cfg(feature = "ssr")] + { + let q = question.trim(); + if q.is_empty() { + return Err(ServerFnError::new("question is empty")); + } + let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| { + ServerFnError::new("ANTHROPIC_API_KEY not set on server") + })?; + + let system = format!( + "You are a reading companion for a philosophical substance called \ + design-dna. A reader is studying the distillation of {source_slug} \ + and has highlighted a passage. Answer their question grounded in the \ + passage and its tradition. Be concise (≤300 words), direct, \ + substance over summary. Same register as the distillation itself: \ + living note, not academic report. Answer in the same language the \ + user asked in. If you don't know, say so." + ); + let user_msg = format!( + "Context passage:\n\n{para_excerpt}\n\n---\n\nQuestion: {q}" + ); + + let answer = call_anthropic(&api_key, &system, &user_msg) + .await + .map_err(|e| ServerFnError::new(format!("claude: {e}")))?; + + // Persist Q+A to marginalia too — reading is a dialogue, and the + // dialogue is the record. + append_marginalia_entry( + &source_slug, + ¶_id, + ¶_excerpt, + MarginaliaPayload::Ask { + question: q.to_string(), + answer: answer.clone(), + }, + &author, + ) + .await?; + spawn_git_autocommit(&source_slug, ¶_id).await; + Ok(answer) + } + #[cfg(not(feature = "ssr"))] + { + unreachable!() + } +} + +// ===================================================================== +// server-only implementations +// ===================================================================== + +#[cfg(feature = "ssr")] +enum MarginaliaPayload { + Note(String), + Ask { question: String, answer: String }, +} + +#[cfg(feature = "ssr")] +async fn append_marginalia_entry( + source_slug: &str, + para_id: &str, + para_excerpt: &str, + payload: MarginaliaPayload, + author: &str, +) -> Result<(), ServerFnError> { + let dir = std::env::var("MANTRA_NOTES_DIR").map_err(|_| { + ServerFnError::new("MANTRA_NOTES_DIR not set on server") + })?; + let dir = std::path::PathBuf::from(&dir); + tokio::fs::create_dir_all(&dir) + .await + .map_err(|e| ServerFnError::new(format!("mkdir: {e}")))?; + let path = dir.join(format!("{source_slug}.md")); + + let existed = path.exists(); + let header_needed = !existed; + let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M UTC").to_string(); + + let mut out = String::new(); + if header_needed { + out.push_str(&format!( + "---\ntype: marginalia\nsource: {source_slug}\nproject: design-dna\n---\n\n# Marginalia · {source_slug}\n\n" + )); + } + + // We always append a fresh block. Simple, no merge logic v0.1. + out.push_str(&format!("## {{#{para_id}}}\n\n")); + let excerpt = para_excerpt.trim(); + if !excerpt.is_empty() { + // Store the first 180 chars of excerpt as a blockquote anchor. + let cut: String = excerpt.chars().take(180).collect(); + out.push_str("> "); + out.push_str(&cut.replace('\n', " ")); + if excerpt.chars().count() > 180 { + out.push_str(" …"); + } + out.push_str("\n\n"); + } + + match payload { + MarginaliaPayload::Note(text) => { + out.push_str(&format!("### {author} · {ts}\n\n{text}\n\n")); + } + MarginaliaPayload::Ask { question, answer } => { + out.push_str(&format!( + "### {author} · {ts} (asked)\n\n**Q:** {question}\n\n**A (Claude):** {answer}\n\n" + )); + } + } + + use tokio::io::AsyncWriteExt; + let mut f = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .await + .map_err(|e| ServerFnError::new(format!("open notes: {e}")))?; + f.write_all(out.as_bytes()) + .await + .map_err(|e| ServerFnError::new(format!("write notes: {e}")))?; + Ok(()) +} + +#[cfg(feature = "ssr")] +fn parse_marginalia(raw: &str) -> Vec { + // Structure we parse: + // ## {#} + // > excerpt + // + // ### Alexey · 2026-04-23 12:34 + // Note body. + // + // ### Alexey · 2026-04-23 13:00 (asked) + // **Q:** ... + // + // **A (Claude):** ... + + let mut entries: Vec = Vec::new(); + let mut current_para: Option = None; + let mut current_excerpt: String = String::new(); + let mut current_author_ts: Option = None; + let mut current_kind: NoteKind = NoteKind::Note; + let mut current_body: String = String::new(); + + fn flush( + entries: &mut Vec, + current_para: &Option, + current_excerpt: &str, + current_author_ts: &Option, + current_kind: NoteKind, + current_body: &str, + ) { + if let (Some(para), Some(ats)) = (current_para.clone(), current_author_ts.clone()) { + let body = current_body.trim(); + if !body.is_empty() { + entries.push(NoteEntry { + para_id: para, + excerpt: current_excerpt.trim().to_string(), + author_ts: ats, + kind: current_kind, + body: body.to_string(), + }); + } + } + } + + for line in raw.lines() { + if let Some(rest) = line.strip_prefix("## {#") { + // Flush previous entry. + flush( + &mut entries, + ¤t_para, + ¤t_excerpt, + ¤t_author_ts, + current_kind, + ¤t_body, + ); + current_body.clear(); + current_author_ts = None; + current_kind = NoteKind::Note; + current_excerpt.clear(); + let id = rest.trim_end_matches('}').trim(); + current_para = Some(id.to_string()); + } else if line.starts_with("> ") { + if !current_excerpt.is_empty() { + current_excerpt.push(' '); + } + current_excerpt.push_str(line.trim_start_matches("> ").trim()); + } else if let Some(rest) = line.strip_prefix("### ") { + flush( + &mut entries, + ¤t_para, + ¤t_excerpt, + ¤t_author_ts, + current_kind, + ¤t_body, + ); + current_body.clear(); + let asked = rest.ends_with("(asked)"); + current_kind = if asked { NoteKind::Ask } else { NoteKind::Note }; + let head = rest.trim_end_matches("(asked)").trim(); + current_author_ts = Some(head.to_string()); + } else { + current_body.push_str(line); + current_body.push('\n'); + } + } + flush( + &mut entries, + ¤t_para, + ¤t_excerpt, + ¤t_author_ts, + current_kind, + ¤t_body, + ); + + entries +} + +#[cfg(feature = "ssr")] +async fn call_anthropic( + api_key: &str, + system: &str, + user: &str, +) -> Result { + let body = serde_json::json!({ + "model": "claude-sonnet-4-6", + "max_tokens": 1024, + "system": system, + "messages": [{ "role": "user", "content": user }], + }); + let resp = reqwest::Client::new() + .post("https://api.anthropic.com/v1/messages") + .header("x-api-key", api_key) + .header("anthropic-version", "2023-06-01") + .header("content-type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| format!("request: {e}"))?; + if !resp.status().is_success() { + let st = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("anthropic {st}: {body}")); + } + let v: serde_json::Value = resp.json().await.map_err(|e| format!("decode: {e}"))?; + let text = v["content"][0]["text"] + .as_str() + .ok_or_else(|| "anthropic response missing content[0].text".to_string())?; + Ok(text.to_string()) +} + +#[cfg(feature = "ssr")] +async fn spawn_git_autocommit(source_slug: &str, para_id: &str) { + // Fire-and-forget: we don't want to block the user on git push. + // Only runs if MANTRA_NOTES_GIT_AUTO_PUSH is set (opt-in). + if std::env::var("MANTRA_NOTES_GIT_AUTO_PUSH").is_err() { + return; + } + let dir = match std::env::var("MANTRA_NOTES_DIR") { + Ok(d) => d, + Err(_) => return, + }; + let slug = source_slug.to_string(); + let pid = para_id.to_string(); + tokio::spawn(async move { + let _ = tokio::process::Command::new("sh") + .arg("-c") + .arg(format!( + "cd {dir} && git add -A && git commit -m 'note: {slug} {pid}' && git push 2>&1", + dir = shell_escape(&dir), + slug = shell_escape(&slug), + pid = shell_escape(&pid), + )) + .output() + .await; + }); +} + +#[cfg(feature = "ssr")] +fn shell_escape(s: &str) -> String { + // Good enough for slugs (alphanumeric + dash) and ordinary paths. + s.replace('\'', "'\\''") +} diff --git a/crates/mantra-ui/src/api/types.rs b/crates/mantra-ui/src/api/types.rs new file mode 100644 index 0000000..a4344e5 --- /dev/null +++ b/crates/mantra-ui/src/api/types.rs @@ -0,0 +1,46 @@ +//! Wire types for the margin-layer server fns. + +use serde::{Deserialize, Serialize}; + +use crate::corpus::{Source, Theme}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum NoteKind { + Note, + Ask, +} + +/// One note OR Q+A entry attached to a specific paragraph. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NoteEntry { + pub para_id: String, + pub excerpt: String, + pub author_ts: String, + pub kind: NoteKind, + pub body: String, +} + +/// Snapshot of a single source + its neighbors for the source page. +/// Carries everything the page needs so SSR and hydrate render +/// identically from the same payload. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SourcePageData { + pub source: Source, + pub prev: Option, + pub next: Option, +} + +/// Snapshot for the landing page — works in reading order + themes. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct LandingData { + pub order: Vec, + pub sources: Vec, + pub themes: Vec, +} + +/// Snapshot for the theme page — one theme + contributing sources. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ThemePageData { + pub theme: Theme, + pub contributing: Vec, +} diff --git a/crates/mantra-ui/src/app.rs b/crates/mantra-ui/src/app.rs new file mode 100644 index 0000000..7a6e324 --- /dev/null +++ b/crates/mantra-ui/src/app.rs @@ -0,0 +1,47 @@ +//! Root Leptos component and HTML shell. + +use leptos::config::LeptosOptions; +use leptos::prelude::*; +use leptos_meta::{provide_meta_context, MetaTags, Title}; +use leptos_router::components::*; +use leptos_router::*; + +use crate::pages; + +/// HTML skeleton wrapping the root app. Called by +/// `leptos_axum::LeptosRoutes` to generate the full page SSR. +pub fn shell(options: LeptosOptions) -> impl IntoView { + view! { + + + + + + + + + + + + + + + } +} + +/// Application root — provides meta context, declares routes. +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + + view! { + + <Router> + <Routes fallback=|| view! { <p class="err">"404"</p> }> + <Route path=path!("/") view=pages::landing::Landing /> + <Route path=path!("/source/:slug") view=pages::source::SourcePage /> + <Route path=path!("/theme/:slug") view=pages::theme::ThemePage /> + </Routes> + </Router> + } +} diff --git a/crates/mantra-ui/src/corpus.rs b/crates/mantra-ui/src/corpus.rs new file mode 100644 index 0000000..c8f1d4d --- /dev/null +++ b/crates/mantra-ui/src/corpus.rs @@ -0,0 +1,125 @@ +//! Corpus data types — shared between SSR render pass and UI components. +//! +//! The server crate owns `load_corpus` and populates the structures +//! from disk; the UI crate only reads them. Everything here is +//! `Serialize + Deserialize + Clone` so it survives Leptos Resources +//! and possible future hydration round-trips. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; + +/// The entire loaded corpus. Shared via Leptos context as `Arc<Corpus>`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Corpus { + /// Ordered list of source slugs (reading order — chronological or + /// thematic; driven by `_index.md`'s table of works). + pub order: Vec<String>, + /// Slug → Source lookup. + pub sources: HashMap<String, Source>, + /// Running themes extracted from `_index.md`. + pub themes: Vec<Theme>, +} + +impl Corpus { + pub fn get(&self, slug: &str) -> Option<&Source> { + self.sources.get(slug) + } + + /// Index-in-reading-order of `slug`, if present. + pub fn index_of(&self, slug: &str) -> Option<usize> { + self.order.iter().position(|s| s == slug) + } + + /// Neighbor (prev, next) by reading order — wrap-around. + pub fn neighbors(&self, slug: &str) -> (Option<&str>, Option<&str>) { + let Some(i) = self.index_of(slug) else { return (None, None) }; + let n = self.order.len(); + let prev = if n > 1 { + Some(self.order[(i + n - 1) % n].as_str()) + } else { + None + }; + let next = if n > 1 { + Some(self.order[(i + 1) % n].as_str()) + } else { + None + }; + (prev, next) + } +} + +/// One distillation. Body is pre-rendered HTML (pulldown-cmark output). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Source { + pub slug: String, + /// Display title — "Symposium (Plato)" etc. + pub title: String, + /// Short author label — "Plato", "Ibn ʿArabī" etc. + pub author: String, + /// The core-claim one-liner (extracted from `## Core claim` section). + pub core_claim: String, + /// Tags from YAML frontmatter. + pub tags: Vec<String>, + /// Confidence level (high / medium / low) from frontmatter. + pub confidence: String, + /// The full markdown body rendered to HTML. + pub body_html: String, +} + +/// A running theme — pattern that proustep across multiple sources. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Theme { + pub slug: String, + pub title: String, + /// Short pitch (one paragraph). + pub description_html: String, + /// Sources that contribute to this theme (by slug). + pub contributing: Vec<String>, +} + +/// Convenience alias — we always thread the corpus through context as +/// an `Arc` so multiple concurrent requests share one in-memory copy. +pub type CorpusHandle = Arc<Corpus>; + +/// Reading language. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Lang { + Ru, + En, +} + +impl Lang { + pub fn as_str(&self) -> &'static str { + match self { Lang::Ru => "ru", Lang::En => "en" } + } + pub fn from_query(s: Option<&str>) -> Self { + match s { + Some("en") => Lang::En, + _ => Lang::Ru, + } + } + pub fn other(&self) -> Lang { + match self { Lang::Ru => Lang::En, Lang::En => Lang::Ru } + } +} + +/// Bilingual container — same slugs in both languages. Each language +/// is held as an `Arc<Corpus>` so `for_lang` is cheap and callers can +/// keep their own handle without deep-cloning a 60kw corpus. +#[derive(Debug, Clone)] +pub struct BilingualCorpus { + pub ru: Arc<Corpus>, + pub en: Arc<Corpus>, +} + +impl BilingualCorpus { + pub fn for_lang(&self, lang: Lang) -> Arc<Corpus> { + match lang { + Lang::Ru => self.ru.clone(), + Lang::En => self.en.clone(), + } + } +} + +pub type BilingualHandle = Arc<BilingualCorpus>; diff --git a/crates/mantra-ui/src/lib.rs b/crates/mantra-ui/src/lib.rs new file mode 100644 index 0000000..43679f1 --- /dev/null +++ b/crates/mantra-ui/src/lib.rs @@ -0,0 +1,22 @@ +//! mantra — Leptos UI. +//! +//! Reader for dense markdown distillate corpora. First consumer: the +//! design-dna philosophy corpus (20 sources, ~61k words). The server +//! crate loads the corpus into `Arc<Corpus>` at startup and provides +//! it via context; UI components read from that context. + +#![recursion_limit = "256"] + +pub mod api; +pub mod app; +pub mod corpus; +pub mod pages; + +/// WASM entry point. Called by the cargo-leptos-injected bootstrap +/// script once the bundle has downloaded in the browser. +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + console_error_panic_hook::set_once(); + leptos::mount::hydrate_body(app::App); +} diff --git a/crates/mantra-ui/src/pages/landing.rs b/crates/mantra-ui/src/pages/landing.rs new file mode 100644 index 0000000..c85679c --- /dev/null +++ b/crates/mantra-ui/src/pages/landing.rs @@ -0,0 +1,146 @@ +//! `/` — two-door entry into the corpus: works (20) and themes. + +use leptos::prelude::*; +use leptos_router::hooks::use_query_map; + +use crate::api::{fetch_landing, LandingData}; +use crate::corpus::{Lang, Source, Theme}; +use crate::pages::shared::LangToggle; + +#[component] +pub fn Landing() -> impl IntoView { + let query = use_query_map(); + let lang = Memo::new(move |_| { + Lang::from_query(query.read().get("lang").as_deref()) + }); + + let data = Resource::new( + move || lang.get().as_str().to_string(), + |l| fetch_landing(l), + ); + + let hero_title = move || match lang.get() { + Lang::Ru => "дизайн днк", + Lang::En => "design dna", + }; + let hero_subtitle = move || match lang.get() { + Lang::Ru => "цикл 1 · философия · 20 источников", + Lang::En => "cycle 1 · philosophy · 20 sources", + }; + let hero_question = move || match lang.get() { + Lang::Ru => "почему форма трогает человеческое сердце?", + Lang::En => "why does form move the human heart?", + }; + let works_label = move || match lang.get() { + Lang::Ru => "работы", + Lang::En => "works", + }; + let themes_label = move || match lang.get() { + Lang::Ru => "темы", + Lang::En => "themes", + }; + let sources_suffix = move |n: usize| match lang.get() { + Lang::Ru => format!(" · {n} источников"), + Lang::En => format!(" · {n} sources"), + }; + + view! { + <main class="landing"> + <LangToggle current=lang/> + <header class="landing-head"> + <h1 class="landing-title">{hero_title}</h1> + <p class="landing-subtitle">{hero_subtitle}</p> + <p class="landing-question">{hero_question}</p> + </header> + + <Suspense fallback=move || view! { <p class="landing-loading">"…"</p> }> + {move || { + data.get().map(|res| { + let lang_val = lang.get(); + let suffix_for_themes = sources_suffix.clone(); + match res { + Ok(LandingData { order, sources, themes }) => { + // Build slug -> Source map for cheap lookup in the For body + let mut by_slug: std::collections::HashMap<String, Source> = + std::collections::HashMap::with_capacity(sources.len()); + for s in sources { by_slug.insert(s.slug.clone(), s); } + view! { + <section class="landing-grid"> + <div class="landing-col"> + <h2 class="landing-col-label">{works_label}</h2> + <ul class="works-list"> + <For + each=move || order.clone() + key=|slug| slug.clone() + let:slug + > + <WorkLine + source=by_slug.get(&slug).cloned() + lang=lang_val + slug=slug + /> + </For> + </ul> + </div> + <div class="landing-col"> + <h2 class="landing-col-label">{themes_label}</h2> + <ul class="themes-list"> + <For + each=move || themes.clone() + key=|t| t.slug.clone() + let:theme + > + <ThemeLine + theme=theme + lang=lang_val + suffix=suffix_for_themes.clone() + /> + </For> + </ul> + </div> + </section> + }.into_any() + } + Err(e) => view! { <p class="err">{format!("{e}")}</p> }.into_any(), + } + }) + }} + </Suspense> + </main> + } +} + +#[component] +fn WorkLine(source: Option<Source>, slug: String, lang: Lang) -> impl IntoView { + let href = format!("/source/{slug}?lang={}", lang.as_str()); + match source { + None => view! { <li></li> }.into_any(), + Some(s) => view! { + <li class="work-line"> + <a href=href> + <span class="work-author">{s.author}</span> + <span class="work-sep">" · "</span> + <span class="work-title">{s.title}</span> + <div class="work-claim">{s.core_claim}</div> + </a> + </li> + }.into_any(), + } +} + +#[component] +fn ThemeLine<F>(theme: Theme, lang: Lang, suffix: F) -> impl IntoView +where + F: Fn(usize) -> String + 'static + Send + Sync + Clone, +{ + let href = format!("/theme/{}?lang={}", theme.slug, lang.as_str()); + let count = theme.contributing.len(); + view! { + <li class="theme-line"> + <a href=href> + <span class="theme-title">{theme.title}</span> + <span class="theme-count">{suffix(count)}</span> + </a> + </li> + } +} diff --git a/crates/mantra-ui/src/pages/margin.rs b/crates/mantra-ui/src/pages/margin.rs new file mode 100644 index 0000000..9f889d2 --- /dev/null +++ b/crates/mantra-ui/src/pages/margin.rs @@ -0,0 +1,355 @@ +//! Margin layer — the drawer that opens when a paragraph is clicked. +//! +//! Holds: a Notes tab (write a thought, permanent record in vault) and +//! an Ask tab (ask Claude about the passage; Q+A is also saved to the +//! vault — the dialogue IS the record). +//! +//! Author identity for v0.1: an inline input pinned at the bottom of +//! the drawer, prefilled from `localStorage` under `mantra.author`. +//! No modal prompts. + +use leptos::html; +use leptos::prelude::*; +use leptos::task::spawn_local; + +use crate::api::{ask_claude, fetch_notes, save_note, NoteEntry, NoteKind}; +use crate::corpus::Lang; + +/// A paragraph the user opened the drawer on. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ActivePara { + pub id: String, + pub excerpt: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Tab { + Note, + Ask, +} + +#[component] +pub fn MarginDrawer( + slug: Signal<String>, + lang: Signal<Lang>, + active: ReadSignal<Option<ActivePara>>, + set_active: WriteSignal<Option<ActivePara>>, + /// Bumped whenever the set of notes for this slug changes, so the + /// source page can re-annotate paragraph dots. + notes_tick: RwSignal<u32>, +) -> impl IntoView { + let (notes, set_notes) = signal::<Vec<NoteEntry>>(Vec::new()); + let (tab, set_tab) = signal(Tab::Note); + let (draft, set_draft) = signal(String::new()); + let (pending, set_pending) = signal(false); + let (error, set_error) = signal::<Option<String>>(None); + let (author, set_author) = signal(initial_author()); + + let textarea_ref: NodeRef<html::Textarea> = NodeRef::new(); + + // Refetch all notes for the current slug. Happens on: mount, + // slug change, after save, after ask. + let reload = move |slug_s: String| { + spawn_local(async move { + match fetch_notes(slug_s).await { + Ok(v) => { + set_notes.set(v); + notes_tick.update(|n| *n = n.wrapping_add(1)); + } + Err(e) => { + log(&format!("fetch_notes error: {e}")); + } + } + }); + }; + + // Track slug changes — reload notes. + Effect::new(move |_| { + let s = slug.get(); + if !s.is_empty() { + reload(s); + } + }); + + // Clear draft/error/tab when switching paragraphs. + Effect::new(move |_| { + let _ = active.get(); + set_draft.set(String::new()); + set_error.set(None); + set_tab.set(Tab::Note); + clear_textarea(&textarea_ref); + }); + + let filtered = Memo::new(move |_| { + let Some(a) = active.get() else { return Vec::new() }; + notes.with(|all| { + all.iter().filter(|e| e.para_id == a.id).cloned().collect::<Vec<_>>() + }) + }); + + let close = move |_| set_active.set(None); + + let submit = move || { + log("submit clicked"); + let Some(a) = active.get_untracked() else { + log("submit: no active paragraph"); + return; + }; + let text = draft.get_untracked().trim().to_string(); + if text.is_empty() { + log("submit: empty draft"); + return; + } + let author_name = { + let a = author.get_untracked(); + if a.trim().is_empty() { "anon".to_string() } else { a } + }; + let slug_s = slug.get_untracked(); + let para_id = a.id.clone(); + let excerpt = a.excerpt.clone(); + let current_tab = tab.get_untracked(); + log(&format!( + "submit: tab={:?} slug={} para_id={} text_len={} author={}", + current_tab, slug_s, para_id, text.len(), author_name, + )); + set_pending.set(true); + set_error.set(None); + spawn_local(async move { + let res = match current_tab { + Tab::Note => save_note( + slug_s.clone(), + para_id, + excerpt, + text, + author_name, + ).await.map(|_| ()), + Tab::Ask => ask_claude( + slug_s.clone(), + para_id, + excerpt, + text, + author_name, + ).await.map(|_| ()), + }; + set_pending.set(false); + match res { + Ok(()) => { + log("submit: ok"); + set_draft.set(String::new()); + clear_textarea(&textarea_ref); + reload(slug_s); + } + Err(e) => { + log(&format!("submit: error {e}")); + set_error.set(Some(format!("{e}"))); + } + } + }); + }; + + let label_close = move || match lang.get() { Lang::Ru => "закрыть", Lang::En => "close" }; + let label_note = move || match lang.get() { Lang::Ru => "заметка", Lang::En => "note" }; + let label_ask = move || match lang.get() { Lang::Ru => "спросить", Lang::En => "ask" }; + let placeholder_note = move || match lang.get() { + Lang::Ru => "запишите мысль…", + Lang::En => "write a thought…", + }; + let placeholder_ask = move || match lang.get() { + Lang::Ru => "спросите Клода о пассаже…", + Lang::En => "ask Claude about the passage…", + }; + let label_save = move || match lang.get() { Lang::Ru => "сохранить", Lang::En => "save" }; + let label_ask_go = move || match lang.get() { Lang::Ru => "спросить", Lang::En => "ask" }; + let label_pending_note = move || match lang.get() { + Lang::Ru => "сохраняю…", + Lang::En => "saving…", + }; + let label_pending_ask = move || match lang.get() { + Lang::Ru => "думаю…", + Lang::En => "thinking…", + }; + let label_you = move || match lang.get() { Lang::Ru => "вы:", Lang::En => "you:" }; + + view! { + <Show + when=move || active.get().is_some() + fallback=|| ().into_any() + > + <aside class="margin-drawer"> + <header class="margin-drawer-head"> + <div class="margin-drawer-tabs"> + <button + class="tab" + class:tab-active=move || tab.get() == Tab::Note + on:click=move |_| set_tab.set(Tab::Note) + >{label_note}</button> + <button + class="tab" + class:tab-active=move || tab.get() == Tab::Ask + on:click=move |_| set_tab.set(Tab::Ask) + >{label_ask}</button> + </div> + <button class="margin-drawer-close" on:click=close aria-label=label_close>"×"</button> + </header> + + <div class="margin-drawer-excerpt"> + {move || active.get().map(|a| a.excerpt)} + </div> + + <div class="margin-drawer-form"> + <textarea + class="margin-drawer-input" + node_ref=textarea_ref + placeholder=move || match tab.get() { + Tab::Note => placeholder_note(), + Tab::Ask => placeholder_ask(), + } + on:input=move |ev| set_draft.set(event_target_value(&ev)) + rows="4" + /> + {move || error.get().map(|e| view! { + <p class="margin-drawer-error">{e}</p> + })} + <div class="margin-drawer-actions"> + <label class="margin-drawer-author"> + <span class="margin-drawer-author-label">{label_you}</span> + <input + type="text" + class="margin-drawer-author-input" + prop:value=move || author.get() + on:input=move |ev| { + let v = event_target_value(&ev); + persist_author(&v); + set_author.set(v); + } + placeholder="anon" + /> + </label> + <button + type="button" + class="margin-drawer-submit" + disabled=move || pending.get() || draft.get().trim().is_empty() + on:click=move |_| submit() + > + {move || { + if pending.get() { + match tab.get() { + Tab::Note => label_pending_note(), + Tab::Ask => label_pending_ask(), + } + } else { + match tab.get() { + Tab::Note => label_save(), + Tab::Ask => label_ask_go(), + } + } + }} + </button> + </div> + </div> + + <div class="margin-drawer-entries"> + {move || { + let list = filtered.get(); + list.into_iter().map(|e| view! { + <NoteCard entry=e/> + }).collect_view() + }} + </div> + </aside> + </Show> + } +} + +#[component] +fn NoteCard(entry: NoteEntry) -> impl IntoView { + let is_ask = entry.kind == NoteKind::Ask; + let cls = if is_ask { "note-card note-card-ask" } else { "note-card" }; + + // For Ask entries the body is `**Q:** ...\n\n**A (Claude):** ...`; + // split so we can style them distinctly. + let (q, a) = if is_ask { + split_ask_body(&entry.body) + } else { + (None, Some(entry.body.clone())) + }; + + view! { + <article class=cls> + <header class="note-card-meta">{entry.author_ts}</header> + {q.map(|s| view! { <p class="note-q">{s}</p> })} + {a.map(|s| view! { <p class="note-a">{s}</p> })} + </article> + } +} + +fn split_ask_body(body: &str) -> (Option<String>, Option<String>) { + let mut q = None; + let mut a = None; + if let Some(q_rest) = body.strip_prefix("**Q:**") { + if let Some((q_part, a_part)) = q_rest.split_once("**A (Claude):**") { + q = Some(q_part.trim().to_string()); + a = Some(a_part.trim().to_string()); + } else { + q = Some(q_rest.trim().to_string()); + } + } else { + a = Some(body.trim().to_string()); + } + (q, a) +} + +// --- Author (inline, no modal prompt) --------------------------------- + +fn initial_author() -> String { + #[cfg(target_arch = "wasm32")] + { + if let Some(win) = web_sys::window() { + if let Ok(Some(storage)) = win.local_storage() { + if let Ok(Some(name)) = storage.get_item("mantra.author") { + if !name.trim().is_empty() { + return name; + } + } + } + } + } + String::new() +} + +fn persist_author(name: &str) { + #[cfg(target_arch = "wasm32")] + { + if let Some(win) = web_sys::window() { + if let Ok(Some(storage)) = win.local_storage() { + let _ = storage.set_item("mantra.author", name); + } + } + } + #[cfg(not(target_arch = "wasm32"))] + { let _ = name; } +} + +// --- Textarea helpers ------------------------------------------------- + +fn clear_textarea(node: &NodeRef<html::Textarea>) { + #[cfg(target_arch = "wasm32")] + { + if let Some(el) = node.get_untracked() { + el.set_value(""); + } + } + #[cfg(not(target_arch = "wasm32"))] + { let _ = node; } +} + +// --- Debug log -------------------------------------------------------- + +fn log(msg: &str) { + #[cfg(target_arch = "wasm32")] + { + web_sys::console::log_1(&format!("[mantra] {msg}").into()); + } + #[cfg(not(target_arch = "wasm32"))] + { let _ = msg; } +} diff --git a/crates/mantra-ui/src/pages/mod.rs b/crates/mantra-ui/src/pages/mod.rs new file mode 100644 index 0000000..026aad8 --- /dev/null +++ b/crates/mantra-ui/src/pages/mod.rs @@ -0,0 +1,5 @@ +pub mod landing; +pub mod margin; +pub mod shared; +pub mod source; +pub mod theme; diff --git a/crates/mantra-ui/src/pages/shared.rs b/crates/mantra-ui/src/pages/shared.rs new file mode 100644 index 0000000..c70c85f --- /dev/null +++ b/crates/mantra-ui/src/pages/shared.rs @@ -0,0 +1,37 @@ +//! Shared UI atoms. + +use leptos::prelude::*; +use leptos_router::hooks::{use_location, use_query_map}; + +use crate::corpus::Lang; + +/// Minimal top-right language toggle. Two links; current is dimmed, +/// the other is hot. Preserves the rest of the URL (path + remaining +/// query params) so toggling on a deep page keeps you there. +#[component] +pub fn LangToggle(current: Memo<Lang>) -> impl IntoView { + let location = use_location(); + let query = use_query_map(); + + let make_href = move |target: Lang| { + let path = location.pathname.get(); + let mut params = query.get(); + params.replace("lang", target.as_str().to_string()); + let qs = params.to_query_string(); + if qs.is_empty() { path } else { format!("{path}{qs}") } + }; + + view! { + <nav class="lang-toggle"> + <a + href=move || make_href(Lang::Ru) + class:lang-active=move || current.get() == Lang::Ru + >"ru"</a> + <span class="lang-dot">"·"</span> + <a + href=move || make_href(Lang::En) + class:lang-active=move || current.get() == Lang::En + >"en"</a> + </nav> + } +} diff --git a/crates/mantra-ui/src/pages/source.rs b/crates/mantra-ui/src/pages/source.rs new file mode 100644 index 0000000..3b04a82 --- /dev/null +++ b/crates/mantra-ui/src/pages/source.rs @@ -0,0 +1,194 @@ +//! `/source/:slug` — one distillation, book-page typography. +//! +//! Reads its data via the `source_page` server fn wrapped in a +//! `Resource` so SSR and hydrate render from the same payload. +//! This avoids needing the corpus `Arc<Corpus>` context on the +//! client (where it isn't available). + +use leptos::prelude::*; +use leptos_router::hooks::{use_params, use_query_map}; +use leptos_router::params::Params; + +use crate::api::{fetch_source_page, SourcePageData}; +use crate::corpus::Lang; +use crate::pages::margin::{ActivePara, MarginDrawer}; +use crate::pages::shared::LangToggle; + +#[derive(Params, PartialEq, Clone, Debug)] +struct SlugParam { + slug: Option<String>, +} + +#[component] +pub fn SourcePage() -> impl IntoView { + let params = use_params::<SlugParam>(); + let slug = Memo::new(move |_| { + params.get().ok().and_then(|p| p.slug).unwrap_or_default() + }); + + let query = use_query_map(); + let lang = Memo::new(move |_| { + Lang::from_query(query.read().get("lang").as_deref()) + }); + + let (active, set_active) = signal::<Option<ActivePara>>(None); + let notes_tick = RwSignal::new(0u32); + + let slug_sig: Signal<String> = Signal::derive(move || slug.get()); + let lang_sig: Signal<Lang> = Signal::derive(move || lang.get()); + + // Resource key = (slug, lang). Re-fetches when either changes. + let data = Resource::new( + move || (slug.get(), lang.get().as_str().to_string()), + |(s, l)| fetch_source_page(s, l), + ); + + // After hydration + notes load: annotate paragraphs that have + // marginalia so a dot appears in the left gutter. + #[cfg(feature = "hydrate")] + { + let slug_for_fx = slug_sig; + Effect::new(move |_| { + let _ = notes_tick.get(); + let current_slug = slug_for_fx.get(); + if current_slug.is_empty() { return; } + leptos::task::spawn_local(async move { + if let Ok(entries) = crate::api::fetch_notes(current_slug).await { + let ids: std::collections::HashSet<String> = + entries.into_iter().map(|e| e.para_id).collect(); + annotate_notes(&ids); + } + }); + }); + } + + view! { + <LangToggle current=lang/> + <Suspense fallback=move || view! { <p class="source-loading">"…"</p> }> + {move || { + data.get().map(|res| { + let lang_val = lang.get(); + let home_href = format!("/?lang={}", lang_val.as_str()); + let breadcrumb_cycle = match lang_val { + Lang::Ru => "цикл 1", + Lang::En => "cycle 1", + }; + let label_all = match lang_val { + Lang::Ru => "все источники", + Lang::En => "all sources", + }; + let label_prev = match lang_val { + Lang::Ru => "← предыдущий", + Lang::En => "← prev", + }; + let label_next = match lang_val { + Lang::Ru => "следующий →", + Lang::En => "next →", + }; + + match res { + Ok(Some(d)) => { + let SourcePageData { source: src, prev, next } = d; + let prev_href = prev.map(|p| format!("/source/{p}?lang={}", lang_val.as_str())); + let next_href = next.map(|n| format!("/source/{n}?lang={}", lang_val.as_str())); + view! { + <main class="source"> + <nav class="source-breadcrumb"> + <a href=home_href.clone()>"design dna"</a> + <span class="sep">" · "</span> + <span>{breadcrumb_cycle}</span> + <span class="sep">" · "</span> + <span class="source-author">{src.author}</span> + </nav> + + <article + class="source-body" + inner_html=src.body_html + on:click=move |ev| { + if let Some(p) = paragraph_from_event(&ev) { + set_active.set(Some(p)); + } + } + ></article> + + <footer class="source-foot"> + {prev_href.map(|h| view! { + <a class="nav-prev" href=h>{label_prev}</a> + })} + <a class="nav-home" href=home_href>{label_all}</a> + {next_href.map(|h| view! { + <a class="nav-next" href=h>{label_next}</a> + })} + </footer> + </main> + }.into_any() + } + Ok(None) => view! { + <main class="source not-found"> + <p>"source not found"</p> + <a class="back-link" href=home_href>"← back"</a> + </main> + }.into_any(), + Err(e) => view! { + <main class="source not-found"> + <p class="err">{format!("{e}")}</p> + </main> + }.into_any(), + } + }) + }} + </Suspense> + + <MarginDrawer + slug=slug_sig + lang=lang_sig + active=active + set_active=set_active + notes_tick=notes_tick + /> + } +} + +/// Climb up from the click target to find the nearest `[data-para-id]` +/// element; return its id + plain-text content to seed the drawer. +#[cfg(feature = "hydrate")] +fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option<ActivePara> { + use wasm_bindgen::JsCast; + let target = ev.target()?; + let mut el = target.dyn_into::<web_sys::Element>().ok()?; + loop { + if el.has_attribute("data-para-id") { + let id = el.get_attribute("data-para-id")?; + let text = el.text_content().unwrap_or_default(); + let excerpt = text.trim().to_string(); + return Some(ActivePara { id, excerpt }); + } + el = el.parent_element()?; + } +} + +#[cfg(not(feature = "hydrate"))] +fn paragraph_from_event(_ev: &leptos::ev::MouseEvent) -> Option<ActivePara> { + None +} + +/// Add `class="has-notes"` to every paragraph that has at least one +/// marginalia entry. Called after notes fetch. +#[cfg(feature = "hydrate")] +fn annotate_notes(ids: &std::collections::HashSet<String>) { + use wasm_bindgen::JsCast; + let Some(win) = web_sys::window() else { return }; + let Some(doc) = win.document() else { return }; + let Ok(nodes) = doc.query_selector_all("[data-para-id]") else { return }; + for i in 0..nodes.length() { + let Some(node) = nodes.item(i) else { continue }; + let Ok(el) = node.dyn_into::<web_sys::Element>() else { continue }; + let id = el.get_attribute("data-para-id").unwrap_or_default(); + let classes = el.class_list(); + if ids.contains(&id) { + let _ = classes.add_1("has-notes"); + } else { + let _ = classes.remove_1("has-notes"); + } + } +} diff --git a/crates/mantra-ui/src/pages/theme.rs b/crates/mantra-ui/src/pages/theme.rs new file mode 100644 index 0000000..4c53e80 --- /dev/null +++ b/crates/mantra-ui/src/pages/theme.rs @@ -0,0 +1,117 @@ +//! `/theme/:slug` — one running theme, with contributing sources. + +use leptos::prelude::*; +use leptos_router::hooks::{use_params, use_query_map}; +use leptos_router::params::Params; + +use crate::api::{fetch_theme_page, ThemePageData}; +use crate::corpus::{Lang, Source}; +use crate::pages::shared::LangToggle; + +#[derive(Params, PartialEq, Clone, Debug)] +struct SlugParam { + slug: Option<String>, +} + +#[component] +pub fn ThemePage() -> impl IntoView { + let params = use_params::<SlugParam>(); + let slug = Memo::new(move |_| { + params.get().ok().and_then(|p| p.slug).unwrap_or_default() + }); + + let query = use_query_map(); + let lang = Memo::new(move |_| { + Lang::from_query(query.read().get("lang").as_deref()) + }); + + let data = Resource::new( + move || (slug.get(), lang.get().as_str().to_string()), + |(s, l)| fetch_theme_page(s, l), + ); + + view! { + <LangToggle current=lang/> + <Suspense fallback=move || view! { <p class="source-loading">"…"</p> }> + {move || { + data.get().map(|res| { + let lang_val = lang.get(); + let home_href = format!("/?lang={}", lang_val.as_str()); + let label_back = match lang_val { + Lang::Ru => "← все темы", + Lang::En => "← all themes", + }; + let label_sources = match lang_val { + Lang::Ru => "источники", + Lang::En => "sources", + }; + let theme_label = match lang_val { + Lang::Ru => "тема", + Lang::En => "theme", + }; + + match res { + Ok(Some(ThemePageData { theme, contributing })) => view! { + <main class="theme"> + <nav class="source-breadcrumb"> + <a href=home_href.clone()>"design dna"</a> + <span class="sep">" · "</span> + <span>{theme_label}</span> + </nav> + <h1 class="theme-h1">{theme.title}</h1> + <div + class="theme-desc" + inner_html=theme.description_html + ></div> + <section class="theme-contributing"> + <h3 class="landing-col-label">{label_sources}</h3> + <ul class="works-list"> + <For + each=move || contributing.clone() + key=|s| s.slug.clone() + let:source + > + <ContributingLine + source=source + lang=lang_val + /> + </For> + </ul> + </section> + <footer class="source-foot"> + <a class="nav-home" href=home_href>{label_back}</a> + </footer> + </main> + }.into_any(), + Ok(None) => view! { + <main class="theme not-found"> + <p>"theme not found"</p> + <a class="back-link" href=home_href>"← back"</a> + </main> + }.into_any(), + Err(e) => view! { + <main class="theme not-found"> + <p class="err">{format!("{e}")}</p> + </main> + }.into_any(), + } + }) + }} + </Suspense> + } +} + +#[component] +fn ContributingLine(source: Source, lang: Lang) -> impl IntoView { + let href = format!("/source/{}?lang={}", source.slug, lang.as_str()); + view! { + <li class="work-line"> + <a href=href> + <span class="work-author">{source.author}</span> + <span class="work-sep">" · "</span> + <span class="work-title">{source.title}</span> + <div class="work-claim">{source.core_claim}</div> + </a> + </li> + } +} diff --git a/sass/main.scss b/sass/main.scss new file mode 100644 index 0000000..9edb808 --- /dev/null +++ b/sass/main.scss @@ -0,0 +1,756 @@ +// mantra · book-grade reader typography +// Fraunces (variable) body + IBM Plex Sans (labels) + IBM Plex Mono (non-Latin) +// Pergament-ink light · deep-ink dark · 62ch measure +// Headings H2 as margin-labels on wide, inline-before on mobile + +// --- Fonts (self-hosted) --------------------------------------------- + +@font-face { + font-family: "Fraunces"; + src: url("/fonts/fraunces-variable.woff2") format("woff2-variations"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "Fraunces"; + src: url("/fonts/fraunces-italic-variable.woff2") format("woff2-variations"); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} +@font-face { + font-family: "IBM Plex Sans"; + src: url("/fonts/plex-sans-variable.woff2") format("woff2-variations"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "IBM Plex Mono"; + src: url("/fonts/plex-mono-variable.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +// --- Color tokens ---------------------------------------------------- + +:root { + // Pergament (light) + --mantra-bg: #fdfbf5; + --mantra-fg: #1c1917; + --mantra-muted: #78716c; + --mantra-faint: #d6d3d0; + --mantra-accent: #5e4b3a; + --mantra-hairline: rgba(28, 25, 23, 0.08); + + --mantra-measure: 62ch; + --mantra-serif: "Fraunces", Georgia, "Times New Roman", serif; + --mantra-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --mantra-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; +} +@media (prefers-color-scheme: dark) { + :root { + --mantra-bg: #0f1012; + --mantra-fg: #e7e3dd; + --mantra-muted: #8a857d; + --mantra-faint: #2a2826; + --mantra-accent: #c9b999; + --mantra-hairline: rgba(231, 227, 221, 0.10); + } +} + +// --- Reset + base ---------------------------------------------------- + +* { box-sizing: border-box; } + +::selection { + background: var(--mantra-accent); + color: var(--mantra-bg); +} + +html, body { + margin: 0; + padding: 0; + background: var(--mantra-bg); + color: var(--mantra-fg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + font-family: var(--mantra-serif); + font-size: 19px; + line-height: 1.68; + letter-spacing: 0.002em; + // Variable-axis tuning: a warmer serif without eccentricity + font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 400; + font-feature-settings: "kern" 1, "liga" 1, "calt" 1, "onum" 1; +} + +a { + color: inherit; + text-decoration: none; + border-bottom: 1px solid var(--mantra-hairline); + transition: border-color 0.2s, color 0.2s; +} +a:hover { border-bottom-color: var(--mantra-accent); color: var(--mantra-accent); } + +main { + max-width: var(--mantra-measure); + margin: 0 auto; + padding: 6rem 1.5rem 8rem; +} +@media (max-width: 640px) { + main { padding: 3rem 1.25rem 5rem; } + body { font-size: 18px; line-height: 1.65; } +} + +// --- Landing -------------------------------------------------------- + +.landing { + max-width: 72ch; +} + +.landing-head { margin-bottom: 4rem; } + +.landing-title { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 144, "SOFT" 50, "WONK" 0, "wght" 350; + font-size: clamp(3rem, 7vw, 4.5rem); + font-weight: 350; + line-height: 1.02; + letter-spacing: -0.02em; + margin: 0; + color: var(--mantra-fg); +} + +.landing-subtitle { + font-family: var(--mantra-sans); + font-variation-settings: "wght" 400; + font-size: 0.78rem; + color: var(--mantra-muted); + text-transform: uppercase; + letter-spacing: 0.22em; + margin: 1rem 0 3rem; +} + +.landing-question { + font-family: var(--mantra-serif); + font-style: italic; + font-variation-settings: "opsz" 36, "SOFT" 50, "WONK" 0, "wght" 380; + font-size: 1.4rem; + line-height: 1.4; + color: var(--mantra-fg); + margin: 0 0 4rem; + max-width: 34ch; +} + +.landing-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3.5rem; +} +@media (max-width: 760px) { + .landing-grid { grid-template-columns: 1fr; gap: 3rem; } +} + +.landing-col-label { + font-family: var(--mantra-sans); + font-variation-settings: "wght" 500; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.28em; + color: var(--mantra-muted); + margin: 0 0 1.75rem; +} + +.works-list, .themes-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.work-line { + a { + display: block; + border-bottom: none; + line-height: 1.35; + } + a:hover { color: var(--mantra-fg); } + a:hover .work-author { color: var(--mantra-accent); } +} +.work-author { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 500; + color: var(--mantra-fg); +} +.work-sep { color: var(--mantra-faint); } +.work-title { + font-style: italic; + color: var(--mantra-muted); +} +.work-claim { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 14, "SOFT" 30, "WONK" 0, "wght" 380; + font-size: 0.88rem; + color: var(--mantra-muted); + line-height: 1.5; + margin-top: 0.3rem; +} + +.theme-line a { + display: block; + border-bottom: none; +} +.theme-title { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 500; +} +.theme-count { + font-family: var(--mantra-sans); + color: var(--mantra-muted); + font-size: 0.82rem; + letter-spacing: 0.02em; +} + +// --- Source page ---------------------------------------------------- + +.source-breadcrumb { + font-family: var(--mantra-sans); + font-variation-settings: "wght" 450; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.22em; + color: var(--mantra-muted); + margin-bottom: 3rem; + .sep { margin: 0 0.5em; color: var(--mantra-faint); } + a { color: var(--mantra-muted); border-bottom: none; } + a:hover { color: var(--mantra-fg); } +} + +.source-body { + // Positioning anchor for H2 margin-labels below. + position: relative; + + // H1 is the work title + h1 { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 96, "SOFT" 50, "WONK" 0, "wght" 380; + font-size: clamp(2rem, 5vw, 2.75rem); + line-height: 1.1; + letter-spacing: -0.012em; + margin: 0 0 2.5rem; + color: var(--mantra-fg); + } + + // H2 as margin-label on wide screens (pulled into left margin) + h2 { + font-family: var(--mantra-sans); + font-variation-settings: "wght" 500; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.24em; + color: var(--mantra-muted); + font-weight: 500; + margin: 3.5rem 0 1.25rem; + position: relative; + } + @media (min-width: 1100px) { + h2 { + position: absolute; + // Pull the label into the left gutter, outside the article box. + // 16ch for the label + 2.5rem breathing room before the text + // column (which starts at the article's padding-left, i.e. 0). + left: calc(-16ch - 2.5rem); + margin: 0; + padding-top: 0.5rem; + width: 16ch; + text-align: right; + } + // Insert spacing where H2 would have been inline + h2 + * { margin-top: 3rem; } + } + + h3 { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 24, "SOFT" 40, "WONK" 0, "wght" 520; + font-size: 1.12rem; + letter-spacing: -0.005em; + margin: 2.2rem 0 0.8rem; + color: var(--mantra-fg); + } + + p { + margin: 0 0 1.35rem; + hyphens: auto; + -webkit-hyphens: auto; + } + + em, i { + font-style: italic; + font-variation-settings: "opsz" 18, "SOFT" 40, "WONK" 0, "wght" 400; + } + strong, b { + font-variation-settings: "opsz" 18, "SOFT" 30, "WONK" 0, "wght" 600; + } + + a { + border-bottom: 1px solid var(--mantra-muted); + } + a:hover { border-bottom-color: var(--mantra-accent); } + + blockquote { + border-left: 2px solid var(--mantra-accent); + margin: 2rem 0 2rem -0.5rem; + padding: 0.3rem 0 0.3rem 1.5rem; + font-style: italic; + font-variation-settings: "opsz" 17, "SOFT" 50, "WONK" 0, "wght" 400; + color: var(--mantra-fg); + font-size: 0.98rem; + p { margin-bottom: 0.6rem; &:last-child { margin-bottom: 0; } } + } + + code { + font-family: var(--mantra-mono); + font-size: 0.85em; + background: var(--mantra-faint); + padding: 0.1rem 0.35rem; + border-radius: 3px; + } + + pre { + font-family: var(--mantra-mono); + font-size: 0.82rem; + background: var(--mantra-faint); + padding: 1rem 1.25rem; + border-radius: 6px; + overflow-x: auto; + margin: 1.75rem 0; + line-height: 1.5; + code { background: none; padding: 0; font-size: inherit; } + } + + ul, ol { + margin: 0 0 1.35rem; + padding-left: 1.5rem; + } + li { margin: 0.3rem 0; } + + hr { + border: none; + border-top: 1px solid var(--mantra-hairline); + margin: 3rem auto; + width: 12ch; + } + + table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + margin: 1.5rem 0; + th, td { + padding: 0.5rem 0.8rem; + border-bottom: 1px solid var(--mantra-hairline); + text-align: left; + vertical-align: top; + } + th { + font-family: var(--mantra-sans); + font-variation-settings: "wght" 500; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--mantra-muted); + } + } + + // Paragraph click-target + margin dot (M.3 margin-layer). + // - Empty paragraphs: faint dot appears only on hover (affordance). + // - Annotated paragraphs (.has-notes): dot is always visible in the + // accent colour, so readers see at a glance where the marginalia is. + p[data-para-id] { + position: relative; + cursor: pointer; + } + @media (min-width: 900px) { + p[data-para-id]::before { + content: ""; + position: absolute; + left: -1.25rem; + top: 0.75em; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--mantra-faint); + opacity: 0; + transition: opacity 0.2s, background 0.2s, transform 0.2s; + } + p[data-para-id]:hover::before { + opacity: 1; + background: var(--mantra-muted); + } + p[data-para-id].has-notes::before { + opacity: 1; + background: var(--mantra-accent); + } + p[data-para-id].has-notes:hover::before { + transform: scale(1.3); + } + } +} + +.source-foot { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 1rem; + margin-top: 5rem; + padding-top: 2rem; + border-top: 1px solid var(--mantra-hairline); + font-family: var(--mantra-sans); + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--mantra-muted); + + a { border-bottom: none; color: var(--mantra-muted); } + a:hover { color: var(--mantra-fg); } + + .nav-prev { justify-self: start; } + .nav-home { justify-self: center; } + .nav-next { justify-self: end; } +} + +// --- Theme page ----------------------------------------------------- + +.theme-h1 { + font-family: var(--mantra-serif); + font-variation-settings: "opsz" 72, "SOFT" 50, "WONK" 0, "wght" 380; + font-size: clamp(2rem, 4.5vw, 2.75rem); + line-height: 1.1; + letter-spacing: -0.01em; + margin: 0 0 2rem; + color: var(--mantra-fg); +} + +.theme-desc { + margin-bottom: 3rem; + p { color: var(--mantra-fg); margin-bottom: 1.2rem; } +} + +.theme-contributing { margin-top: 3rem; } + +// --- Misc ----------------------------------------------------------- + +// --- Language toggle (top-right) --- +.lang-toggle { + position: fixed; + top: 1.25rem; + right: 1.5rem; + z-index: 10; + display: flex; + align-items: center; + gap: 0.4rem; + font-family: var(--mantra-sans); + font-variation-settings: "wght" 450; + font-size: 0.78rem; + letter-spacing: 0.1em; + + a { + color: var(--mantra-faint); + text-decoration: none; + border-bottom: none; + padding: 0.1rem 0.1rem; + transition: color 0.15s; + } + a:hover { color: var(--mantra-fg); } + a.lang-active { + color: var(--mantra-fg); + font-variation-settings: "wght" 600; + } + .lang-dot { + color: var(--mantra-faint); + } +} +@media (max-width: 640px) { + .lang-toggle { top: 1rem; right: 1rem; font-size: 0.72rem; } +} + +.err { + color: #b45309; + font-family: var(--mantra-mono); + font-size: 0.88rem; +} +.not-found { padding: 2rem 0; } +.back-link { + color: var(--mantra-muted); + font-family: var(--mantra-sans); + font-size: 0.85rem; + border-bottom: none; +} + +// --- Margin drawer (M.3) -------------------------------------------- + +.margin-drawer { + position: fixed; + top: 0; + right: 0; + max-height: 100vh; + width: min(420px, 90vw); + background: var(--mantra-bg); + border-left: 1px solid var(--mantra-hairline); + box-shadow: -12px 0 32px rgba(0, 0, 0, 0.04); + z-index: 20; + display: flex; + flex-direction: column; + font-family: var(--mantra-sans); + font-size: 0.92rem; + color: var(--mantra-fg); + animation: drawer-in 0.22s ease-out; + overflow: hidden; +} + +@keyframes drawer-in { + from { transform: translateX(16px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +.margin-drawer-head { + position: relative; + padding: 0.9rem 1.1rem 0.7rem; + border-bottom: 1px solid var(--mantra-hairline); + text-align: center; +} + +.margin-drawer-tabs { + display: inline-flex; + gap: 0.2rem; + + .tab { + background: transparent; + border: none; + cursor: pointer; + font-family: var(--mantra-sans); + font-size: 0.8rem; + font-variation-settings: "wght" 450; + letter-spacing: 0.08em; + text-transform: lowercase; + color: var(--mantra-muted); + padding: 0.3rem 0.9rem; + border-radius: 14px; + transition: color 0.15s, background 0.15s; + + &:hover { color: var(--mantra-fg); } + &.tab-active { + color: var(--mantra-bg); + background: var(--mantra-accent); + font-variation-settings: "wght" 600; + } + } +} + +.margin-drawer-close { + position: absolute; + top: 0.55rem; + right: 0.9rem; + background: transparent; + border: none; + cursor: pointer; + font-family: var(--mantra-serif); + font-size: 1.5rem; + line-height: 1; + color: var(--mantra-muted); + padding: 0.2rem 0.45rem; + transition: color 0.15s; + &:hover { color: var(--mantra-fg); } +} + +.margin-drawer-excerpt { + padding: 0.8rem 1.1rem; + font-family: var(--mantra-serif); + font-style: italic; + font-size: 0.88rem; + line-height: 1.5; + color: var(--mantra-muted); + border-bottom: 1px solid var(--mantra-hairline); + max-height: 6.5em; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; +} + +// Entries sit below the form (reading flows downward: write → record). +// They scroll within their own area when long; when empty, they add +// minimal height so the drawer collapses to just head+excerpt+form. +.margin-drawer-entries { + flex: 0 1 auto; + max-height: 42vh; + overflow-y: auto; + padding: 0.6rem 1.1rem 1rem; + display: flex; + flex-direction: column; + gap: 0.9rem; + border-top: 1px solid var(--mantra-hairline); + + &:empty { display: none; } +} + +.margin-empty { + color: var(--mantra-muted); + font-style: italic; + font-family: var(--mantra-serif); + font-size: 0.9rem; + margin: 1.2rem 0; + text-align: center; +} + +.note-card { + border-left: 2px solid var(--mantra-hairline); + padding: 0.1rem 0 0.1rem 0.8rem; + + &.note-card-ask { border-left-color: var(--mantra-accent); } + + .note-card-meta { + font-size: 0.72rem; + letter-spacing: 0.08em; + color: var(--mantra-muted); + margin-bottom: 0.3rem; + font-variation-settings: "wght" 500; + } + .note-q { + font-family: var(--mantra-serif); + font-style: italic; + font-size: 0.94rem; + line-height: 1.5; + margin: 0 0 0.5rem; + color: var(--mantra-fg); + } + .note-a { + font-family: var(--mantra-serif); + font-size: 0.94rem; + line-height: 1.55; + margin: 0; + color: var(--mantra-fg); + white-space: pre-wrap; + } +} + +.margin-drawer-form { + padding: 0.9rem 1.1rem 1rem; + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.margin-drawer-input { + width: 100%; + resize: vertical; + min-height: 4.5rem; + background: var(--mantra-bg); + color: var(--mantra-fg); + border: 1px solid var(--mantra-hairline); + border-radius: 4px; + padding: 0.55rem 0.7rem; + font-family: var(--mantra-serif); + font-size: 0.95rem; + line-height: 1.45; + transition: border-color 0.15s; + &:focus { + outline: none; + border-color: var(--mantra-accent); + } +} + +.margin-drawer-actions { + display: flex; + align-items: center; + gap: 0.8rem; +} + +.margin-drawer-author { + display: inline-flex; + align-items: center; + gap: 0.35rem; + flex: 1; + min-width: 0; + + .margin-drawer-author-label { + font-size: 0.72rem; + color: var(--mantra-muted); + letter-spacing: 0.06em; + text-transform: lowercase; + font-variation-settings: "wght" 500; + } + .margin-drawer-author-input { + flex: 1; + min-width: 0; + background: transparent; + border: none; + border-bottom: 1px dashed var(--mantra-faint); + font-family: var(--mantra-sans); + font-size: 0.82rem; + color: var(--mantra-fg); + padding: 0.1rem 0 0.15rem; + outline: none; + transition: border-color 0.15s; + &:focus { border-bottom-color: var(--mantra-accent); } + &::placeholder { color: var(--mantra-faint); } + } +} + +.margin-drawer-submit { + background: var(--mantra-accent); + color: var(--mantra-bg); + border: none; + border-radius: 14px; + padding: 0.45rem 1.1rem; + font-family: var(--mantra-sans); + font-size: 0.82rem; + font-variation-settings: "wght" 600; + letter-spacing: 0.06em; + text-transform: lowercase; + cursor: pointer; + transition: opacity 0.15s, transform 0.12s; + + &:hover:not(:disabled) { transform: translateY(-1px); } + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.margin-drawer-error { + color: #b45309; + font-family: var(--mantra-mono); + font-size: 0.78rem; + margin: 0; + word-break: break-word; +} + +// Mobile: drawer becomes a bottom sheet +@media (max-width: 720px) { + .margin-drawer { + top: auto; + right: 0; + left: 0; + bottom: 0; + width: 100vw; + max-height: 75vh; + border-left: none; + border-top: 1px solid var(--mantra-hairline); + box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08); + animation: drawer-up 0.22s ease-out; + } + @keyframes drawer-up { + from { transform: translateY(16px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } +} diff --git a/static/fonts/fraunces-italic-variable.woff2 b/static/fonts/fraunces-italic-variable.woff2 new file mode 100644 index 0000000..1898b02 Binary files /dev/null and b/static/fonts/fraunces-italic-variable.woff2 differ diff --git a/static/fonts/fraunces-variable.woff2 b/static/fonts/fraunces-variable.woff2 new file mode 100644 index 0000000..32e1ccc Binary files /dev/null and b/static/fonts/fraunces-variable.woff2 differ diff --git a/static/fonts/plex-mono-variable.woff2 b/static/fonts/plex-mono-variable.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/static/fonts/plex-mono-variable.woff2 differ diff --git a/static/fonts/plex-sans-variable.woff2 b/static/fonts/plex-sans-variable.woff2 new file mode 100644 index 0000000..b757bc5 Binary files /dev/null and b/static/fonts/plex-sans-variable.woff2 differ