minified/fixed css

Orual 9f507e48 4115b35f

+1859 -225
+347 -6
Cargo.lock
··· 55 56 [[package]] 57 name = "ahash" 58 version = "0.8.12" 59 source = "registry+https://github.com/rust-lang/crates.io-index" 60 checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" ··· 675 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 676 677 [[package]] 678 name = "base64ct" 679 version = "1.8.1" 680 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 744 checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" 745 dependencies = [ 746 "typenum", 747 ] 748 749 [[package]] ··· 908 ] 909 910 [[package]] 911 name = "bytemuck" 912 version = "1.24.0" 913 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1405 1406 [[package]] 1407 name = "const-str" 1408 version = "0.4.3" 1409 source = "registry+https://github.com/rust-lang/crates.io-index" 1410 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" ··· 1414 version = "0.7.1" 1415 source = "registry+https://github.com/rust-lang/crates.io-index" 1416 checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" 1417 1418 [[package]] 1419 name = "const_format" ··· 1745 ] 1746 1747 [[package]] 1748 name = "cssparser-macros" 1749 version = "0.6.1" 1750 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1920 dependencies = [ 1921 "data-encoding", 1922 "syn 2.0.111", 1923 ] 1924 1925 [[package]] ··· 3735 version = "1.3.0" 3736 source = "registry+https://github.com/rust-lang/crates.io-index" 3737 checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 3738 3739 [[package]] 3740 name = "futf" ··· 4440 version = "0.12.3" 4441 source = "registry+https://github.com/rust-lang/crates.io-index" 4442 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 4443 4444 [[package]] 4445 name = "hashbrown" ··· 4447 source = "registry+https://github.com/rust-lang/crates.io-index" 4448 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 4449 dependencies = [ 4450 - "ahash", 4451 "allocator-api2", 4452 ] 4453 ··· 5535 5536 [[package]] 5537 name = "itertools" 5538 version = "0.11.0" 5539 source = "registry+https://github.com/rust-lang/crates.io-index" 5540 checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" ··· 5952 source = "registry+https://github.com/rust-lang/crates.io-index" 5953 checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" 5954 dependencies = [ 5955 - "cssparser", 5956 "html5ever 0.29.1", 5957 "indexmap 2.12.1", 5958 "selectors", ··· 6105 ] 6106 6107 [[package]] 6108 name = "linked-hash-map" 6109 version = "0.5.6" 6110 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6621 source = "registry+https://github.com/rust-lang/crates.io-index" 6622 checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" 6623 dependencies = [ 6624 - "ahash", 6625 "portable-atomic", 6626 ] 6627 ··· 7557 ] 7558 7559 [[package]] 7560 name = "owo-colors" 7561 version = "4.2.3" 7562 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7610 ] 7611 7612 [[package]] 7613 name = "parking" 7614 version = "2.2.1" 7615 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7643 version = "1.0.15" 7644 source = "registry+https://github.com/rust-lang/crates.io-index" 7645 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 7646 7647 [[package]] 7648 name = "pem-rfc7468" ··· 8274 ] 8275 8276 [[package]] 8277 name = "publicsuffix" 8278 version = "2.3.0" 8279 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8343 source = "registry+https://github.com/rust-lang/crates.io-index" 8344 checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" 8345 dependencies = [ 8346 - "ahash", 8347 "equivalent", 8348 "hashbrown 0.16.1", 8349 "parking_lot", ··· 8420 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 8421 8422 [[package]] 8423 name = "rand" 8424 version = "0.7.3" 8425 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8573 version = "0.6.2" 8574 source = "registry+https://github.com/rust-lang/crates.io-index" 8575 checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 8576 8577 [[package]] 8578 name = "redox_syscall" ··· 8659 checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 8660 8661 [[package]] 8662 name = "reqwest" 8663 version = "0.12.26" 8664 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8786 "libc", 8787 "untrusted", 8788 "windows-sys 0.52.0", 8789 ] 8790 8791 [[package]] ··· 9079 version = "1.2.0" 9080 source = "registry+https://github.com/rust-lang/crates.io-index" 9081 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 9082 9083 [[package]] 9084 name = "sec1" ··· 9137 checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" 9138 dependencies = [ 9139 "bitflags 1.3.2", 9140 - "cssparser", 9141 "derive_more 0.99.20", 9142 "fxhash", 9143 "log", ··· 9183 dependencies = [ 9184 "serde_core", 9185 "serde_derive", 9186 ] 9187 9188 [[package]] ··· 9590 version = "3.0.0-rc.5" 9591 source = "registry+https://github.com/rust-lang/crates.io-index" 9592 checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" 9593 9594 [[package]] 9595 name = "simd-adler32" ··· 10342 ] 10343 10344 [[package]] 10345 name = "target-lexicon" 10346 version = "0.12.16" 10347 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11303 checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" 11304 dependencies = [ 11305 "base64 0.22.1", 11306 - "data-url", 11307 "flate2", 11308 "fontdb", 11309 "imagesize", ··· 11378 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 11379 11380 [[package]] 11381 name = "walkdir" 11382 version = "2.5.0" 11383 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11670 "jacquard-identity", 11671 "jacquard-lexicon", 11672 "js-sys", 11673 "lol_alloc", 11674 "loro", 11675 "markdown-weaver", ··· 12683 "wasm-bindgen", 12684 "wasm-bindgen-futures", 12685 "web-sys", 12686 ] 12687 12688 [[package]]
··· 55 56 [[package]] 57 name = "ahash" 58 + version = "0.7.8" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" 61 + dependencies = [ 62 + "getrandom 0.2.16", 63 + "once_cell", 64 + "version_check", 65 + ] 66 + 67 + [[package]] 68 + name = "ahash" 69 version = "0.8.12" 70 source = "registry+https://github.com/rust-lang/crates.io-index" 71 checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" ··· 686 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 687 688 [[package]] 689 + name = "base64-simd" 690 + version = "0.7.0" 691 + source = "registry+https://github.com/rust-lang/crates.io-index" 692 + checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" 693 + dependencies = [ 694 + "simd-abstraction", 695 + ] 696 + 697 + [[package]] 698 name = "base64ct" 699 version = "1.8.1" 700 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 764 checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" 765 dependencies = [ 766 "typenum", 767 + ] 768 + 769 + [[package]] 770 + name = "bitvec" 771 + version = "1.0.1" 772 + source = "registry+https://github.com/rust-lang/crates.io-index" 773 + checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 774 + dependencies = [ 775 + "funty", 776 + "radium", 777 + "tap", 778 + "wyz", 779 ] 780 781 [[package]] ··· 940 ] 941 942 [[package]] 943 + name = "bytecheck" 944 + version = "0.6.12" 945 + source = "registry+https://github.com/rust-lang/crates.io-index" 946 + checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" 947 + dependencies = [ 948 + "bytecheck_derive", 949 + "ptr_meta", 950 + "simdutf8", 951 + ] 952 + 953 + [[package]] 954 + name = "bytecheck_derive" 955 + version = "0.6.12" 956 + source = "registry+https://github.com/rust-lang/crates.io-index" 957 + checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" 958 + dependencies = [ 959 + "proc-macro2", 960 + "quote", 961 + "syn 1.0.109", 962 + ] 963 + 964 + [[package]] 965 name = "bytemuck" 966 version = "1.24.0" 967 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1459 1460 [[package]] 1461 name = "const-str" 1462 + version = "0.3.2" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" 1465 + dependencies = [ 1466 + "const-str-proc-macro", 1467 + ] 1468 + 1469 + [[package]] 1470 + name = "const-str" 1471 version = "0.4.3" 1472 source = "registry+https://github.com/rust-lang/crates.io-index" 1473 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" ··· 1477 version = "0.7.1" 1478 source = "registry+https://github.com/rust-lang/crates.io-index" 1479 checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" 1480 + 1481 + [[package]] 1482 + name = "const-str-proc-macro" 1483 + version = "0.3.2" 1484 + source = "registry+https://github.com/rust-lang/crates.io-index" 1485 + checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a" 1486 + dependencies = [ 1487 + "proc-macro2", 1488 + "quote", 1489 + "syn 1.0.109", 1490 + ] 1491 1492 [[package]] 1493 name = "const_format" ··· 1819 ] 1820 1821 [[package]] 1822 + name = "cssparser" 1823 + version = "0.33.0" 1824 + source = "registry+https://github.com/rust-lang/crates.io-index" 1825 + checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" 1826 + dependencies = [ 1827 + "cssparser-macros", 1828 + "dtoa-short", 1829 + "itoa", 1830 + "phf 0.11.3", 1831 + "smallvec", 1832 + ] 1833 + 1834 + [[package]] 1835 + name = "cssparser-color" 1836 + version = "0.1.0" 1837 + source = "registry+https://github.com/rust-lang/crates.io-index" 1838 + checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" 1839 + dependencies = [ 1840 + "cssparser 0.33.0", 1841 + ] 1842 + 1843 + [[package]] 1844 name = "cssparser-macros" 1845 version = "0.6.1" 1846 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2016 dependencies = [ 2017 "data-encoding", 2018 "syn 2.0.111", 2019 + ] 2020 + 2021 + [[package]] 2022 + name = "data-url" 2023 + version = "0.1.1" 2024 + source = "registry+https://github.com/rust-lang/crates.io-index" 2025 + checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" 2026 + dependencies = [ 2027 + "matches", 2028 ] 2029 2030 [[package]] ··· 3840 version = "1.3.0" 3841 source = "registry+https://github.com/rust-lang/crates.io-index" 3842 checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 3843 + 3844 + [[package]] 3845 + name = "funty" 3846 + version = "2.0.0" 3847 + source = "registry+https://github.com/rust-lang/crates.io-index" 3848 + checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 3849 3850 [[package]] 3851 name = "futf" ··· 4551 version = "0.12.3" 4552 source = "registry+https://github.com/rust-lang/crates.io-index" 4553 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 4554 + dependencies = [ 4555 + "ahash 0.7.8", 4556 + ] 4557 4558 [[package]] 4559 name = "hashbrown" ··· 4561 source = "registry+https://github.com/rust-lang/crates.io-index" 4562 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 4563 dependencies = [ 4564 + "ahash 0.8.12", 4565 "allocator-api2", 4566 ] 4567 ··· 5649 5650 [[package]] 5651 name = "itertools" 5652 + version = "0.10.5" 5653 + source = "registry+https://github.com/rust-lang/crates.io-index" 5654 + checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 5655 + dependencies = [ 5656 + "either", 5657 + ] 5658 + 5659 + [[package]] 5660 + name = "itertools" 5661 version = "0.11.0" 5662 source = "registry+https://github.com/rust-lang/crates.io-index" 5663 checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" ··· 6075 source = "registry+https://github.com/rust-lang/crates.io-index" 6076 checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" 6077 dependencies = [ 6078 + "cssparser 0.29.6", 6079 "html5ever 0.29.1", 6080 "indexmap 2.12.1", 6081 "selectors", ··· 6228 ] 6229 6230 [[package]] 6231 + name = "lightningcss" 6232 + version = "1.0.0-alpha.68" 6233 + source = "registry+https://github.com/rust-lang/crates.io-index" 6234 + checksum = "b407ca668368d1d5a86cea58ac82d9f9f9ca4bac1e9dce6f16f875f0f081a911" 6235 + dependencies = [ 6236 + "ahash 0.8.12", 6237 + "bitflags 2.10.0", 6238 + "const-str 0.3.2", 6239 + "cssparser 0.33.0", 6240 + "cssparser-color", 6241 + "dashmap 5.5.3", 6242 + "data-encoding", 6243 + "getrandom 0.3.4", 6244 + "indexmap 2.12.1", 6245 + "itertools 0.10.5", 6246 + "lazy_static", 6247 + "lightningcss-derive", 6248 + "parcel_selectors", 6249 + "parcel_sourcemap", 6250 + "pastey", 6251 + "pathdiff", 6252 + "rayon", 6253 + "serde", 6254 + "serde-content", 6255 + "smallvec", 6256 + ] 6257 + 6258 + [[package]] 6259 + name = "lightningcss-derive" 6260 + version = "1.0.0-alpha.43" 6261 + source = "registry+https://github.com/rust-lang/crates.io-index" 6262 + checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252" 6263 + dependencies = [ 6264 + "convert_case 0.6.0", 6265 + "proc-macro2", 6266 + "quote", 6267 + "syn 1.0.109", 6268 + ] 6269 + 6270 + [[package]] 6271 name = "linked-hash-map" 6272 version = "0.5.6" 6273 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6784 source = "registry+https://github.com/rust-lang/crates.io-index" 6785 checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" 6786 dependencies = [ 6787 + "ahash 0.8.12", 6788 "portable-atomic", 6789 ] 6790 ··· 7720 ] 7721 7722 [[package]] 7723 + name = "outref" 7724 + version = "0.1.0" 7725 + source = "registry+https://github.com/rust-lang/crates.io-index" 7726 + checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" 7727 + 7728 + [[package]] 7729 name = "owo-colors" 7730 version = "4.2.3" 7731 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7779 ] 7780 7781 [[package]] 7782 + name = "parcel_selectors" 7783 + version = "0.28.2" 7784 + source = "registry+https://github.com/rust-lang/crates.io-index" 7785 + checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196" 7786 + dependencies = [ 7787 + "bitflags 2.10.0", 7788 + "cssparser 0.33.0", 7789 + "log", 7790 + "phf 0.11.3", 7791 + "phf_codegen 0.11.3", 7792 + "precomputed-hash", 7793 + "rustc-hash 2.1.1", 7794 + "smallvec", 7795 + ] 7796 + 7797 + [[package]] 7798 + name = "parcel_sourcemap" 7799 + version = "2.1.1" 7800 + source = "registry+https://github.com/rust-lang/crates.io-index" 7801 + checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb" 7802 + dependencies = [ 7803 + "base64-simd", 7804 + "data-url 0.1.1", 7805 + "rkyv", 7806 + "serde", 7807 + "serde_json", 7808 + "vlq", 7809 + ] 7810 + 7811 + [[package]] 7812 name = "parking" 7813 version = "2.2.1" 7814 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7842 version = "1.0.15" 7843 source = "registry+https://github.com/rust-lang/crates.io-index" 7844 checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 7845 + 7846 + [[package]] 7847 + name = "pastey" 7848 + version = "0.1.1" 7849 + source = "registry+https://github.com/rust-lang/crates.io-index" 7850 + checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 7851 + 7852 + [[package]] 7853 + name = "pathdiff" 7854 + version = "0.2.3" 7855 + source = "registry+https://github.com/rust-lang/crates.io-index" 7856 + checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 7857 7858 [[package]] 7859 name = "pem-rfc7468" ··· 8485 ] 8486 8487 [[package]] 8488 + name = "ptr_meta" 8489 + version = "0.1.4" 8490 + source = "registry+https://github.com/rust-lang/crates.io-index" 8491 + checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" 8492 + dependencies = [ 8493 + "ptr_meta_derive", 8494 + ] 8495 + 8496 + [[package]] 8497 + name = "ptr_meta_derive" 8498 + version = "0.1.4" 8499 + source = "registry+https://github.com/rust-lang/crates.io-index" 8500 + checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" 8501 + dependencies = [ 8502 + "proc-macro2", 8503 + "quote", 8504 + "syn 1.0.109", 8505 + ] 8506 + 8507 + [[package]] 8508 name = "publicsuffix" 8509 version = "2.3.0" 8510 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8574 source = "registry+https://github.com/rust-lang/crates.io-index" 8575 checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" 8576 dependencies = [ 8577 + "ahash 0.8.12", 8578 "equivalent", 8579 "hashbrown 0.16.1", 8580 "parking_lot", ··· 8651 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 8652 8653 [[package]] 8654 + name = "radium" 8655 + version = "0.7.0" 8656 + source = "registry+https://github.com/rust-lang/crates.io-index" 8657 + checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 8658 + 8659 + [[package]] 8660 name = "rand" 8661 version = "0.7.3" 8662 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8810 version = "0.6.2" 8811 source = "registry+https://github.com/rust-lang/crates.io-index" 8812 checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" 8813 + 8814 + [[package]] 8815 + name = "rayon" 8816 + version = "1.11.0" 8817 + source = "registry+https://github.com/rust-lang/crates.io-index" 8818 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 8819 + dependencies = [ 8820 + "either", 8821 + "rayon-core", 8822 + ] 8823 + 8824 + [[package]] 8825 + name = "rayon-core" 8826 + version = "1.13.0" 8827 + source = "registry+https://github.com/rust-lang/crates.io-index" 8828 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 8829 + dependencies = [ 8830 + "crossbeam-deque", 8831 + "crossbeam-utils", 8832 + ] 8833 8834 [[package]] 8835 name = "redox_syscall" ··· 8916 checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 8917 8918 [[package]] 8919 + name = "rend" 8920 + version = "0.4.2" 8921 + source = "registry+https://github.com/rust-lang/crates.io-index" 8922 + checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" 8923 + dependencies = [ 8924 + "bytecheck", 8925 + ] 8926 + 8927 + [[package]] 8928 name = "reqwest" 8929 version = "0.12.26" 8930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 9052 "libc", 9053 "untrusted", 9054 "windows-sys 0.52.0", 9055 + ] 9056 + 9057 + [[package]] 9058 + name = "rkyv" 9059 + version = "0.7.45" 9060 + source = "registry+https://github.com/rust-lang/crates.io-index" 9061 + checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" 9062 + dependencies = [ 9063 + "bitvec", 9064 + "bytecheck", 9065 + "bytes", 9066 + "hashbrown 0.12.3", 9067 + "ptr_meta", 9068 + "rend", 9069 + "rkyv_derive", 9070 + "seahash", 9071 + "tinyvec", 9072 + "uuid", 9073 + ] 9074 + 9075 + [[package]] 9076 + name = "rkyv_derive" 9077 + version = "0.7.45" 9078 + source = "registry+https://github.com/rust-lang/crates.io-index" 9079 + checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" 9080 + dependencies = [ 9081 + "proc-macro2", 9082 + "quote", 9083 + "syn 1.0.109", 9084 ] 9085 9086 [[package]] ··· 9374 version = "1.2.0" 9375 source = "registry+https://github.com/rust-lang/crates.io-index" 9376 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 9377 + 9378 + [[package]] 9379 + name = "seahash" 9380 + version = "4.1.0" 9381 + source = "registry+https://github.com/rust-lang/crates.io-index" 9382 + checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 9383 9384 [[package]] 9385 name = "sec1" ··· 9438 checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" 9439 dependencies = [ 9440 "bitflags 1.3.2", 9441 + "cssparser 0.29.6", 9442 "derive_more 0.99.20", 9443 "fxhash", 9444 "log", ··· 9484 dependencies = [ 9485 "serde_core", 9486 "serde_derive", 9487 + ] 9488 + 9489 + [[package]] 9490 + name = "serde-content" 9491 + version = "0.1.2" 9492 + source = "registry+https://github.com/rust-lang/crates.io-index" 9493 + checksum = "3753ca04f350fa92d00b6146a3555e63c55388c9ef2e11e09bce2ff1c0b509c6" 9494 + dependencies = [ 9495 + "serde", 9496 ] 9497 9498 [[package]] ··· 9900 version = "3.0.0-rc.5" 9901 source = "registry+https://github.com/rust-lang/crates.io-index" 9902 checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" 9903 + 9904 + [[package]] 9905 + name = "simd-abstraction" 9906 + version = "0.7.1" 9907 + source = "registry+https://github.com/rust-lang/crates.io-index" 9908 + checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" 9909 + dependencies = [ 9910 + "outref", 9911 + ] 9912 9913 [[package]] 9914 name = "simd-adler32" ··· 10661 ] 10662 10663 [[package]] 10664 + name = "tap" 10665 + version = "1.0.1" 10666 + source = "registry+https://github.com/rust-lang/crates.io-index" 10667 + checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 10668 + 10669 + [[package]] 10670 name = "target-lexicon" 10671 version = "0.12.16" 10672 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11628 checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" 11629 dependencies = [ 11630 "base64 0.22.1", 11631 + "data-url 0.3.2", 11632 "flate2", 11633 "fontdb", 11634 "imagesize", ··· 11703 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 11704 11705 [[package]] 11706 + name = "vlq" 11707 + version = "0.5.1" 11708 + source = "registry+https://github.com/rust-lang/crates.io-index" 11709 + checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" 11710 + 11711 + [[package]] 11712 name = "walkdir" 11713 version = "2.5.0" 11714 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 12001 "jacquard-identity", 12002 "jacquard-lexicon", 12003 "js-sys", 12004 + "lightningcss", 12005 "lol_alloc", 12006 "loro", 12007 "markdown-weaver", ··· 13015 "wasm-bindgen", 13016 "wasm-bindgen-futures", 13017 "web-sys", 13018 + ] 13019 + 13020 + [[package]] 13021 + name = "wyz" 13022 + version = "0.5.1" 13023 + source = "registry+https://github.com/rust-lang/crates.io-index" 13024 + checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 13025 + dependencies = [ 13026 + "tap", 13027 ] 13028 13029 [[package]]
+4 -1
crates/weaver-app/Cargo.toml
··· 35 web = ["dioxus/web", "dioxus-primitives/web"] 36 desktop = ["dioxus/desktop"] 37 mobile = ["dioxus/mobile"] 38 - server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum", "dep:resvg", "dep:usvg", "dep:tiny-skia", "dep:textwrap", "dep:askama", "dep:fontdb"] 39 collab-worker = ["weaver-common/iroh"] 40 41 ··· 87 textwrap = { version = "0.16", optional = true } 88 askama = { version = "0.12", optional = true } 89 fontdb = { version = "0.22", optional = true } 90 91 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 92 webbrowser = "1.0.6"
··· 35 web = ["dioxus/web", "dioxus-primitives/web"] 36 desktop = ["dioxus/desktop"] 37 mobile = ["dioxus/mobile"] 38 + server = [ "dioxus/server", "dep:jacquard-axum", "dep:axum", "dep:resvg", "dep:usvg", "dep:tiny-skia", "dep:textwrap", "dep:askama", "dep:fontdb", "dep:lightningcss"] 39 collab-worker = ["weaver-common/iroh"] 40 41 ··· 87 textwrap = { version = "0.16", optional = true } 88 askama = { version = "0.12", optional = true } 89 fontdb = { version = "0.22", optional = true } 90 + 91 + # CSS minification (server-only) 92 + lightningcss = { version = "1.0.0-alpha.68", optional = true } 93 94 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 95 webbrowser = "1.0.6"
+3 -1
crates/weaver-app/assets/styling/entry.css
··· 321 border: 1px solid var(--color-border); 322 border-radius: 4px; 323 text-decoration: none; 324 - transition: color 0.2s ease, border-color 0.2s ease; 325 } 326 327 .source-badge:hover {
··· 321 border: 1px solid var(--color-border); 322 border-radius: 4px; 323 text-decoration: none; 324 + transition: 325 + color 0.2s ease, 326 + border-color 0.2s ease; 327 } 328 329 .source-badge:hover {
+1252
crates/weaver-app/assets/styling/notebook-defaults.css
···
··· 1 + /* CSS Reset */ 2 + *, 3 + *::before, 4 + *::after { 5 + box-sizing: border-box; 6 + margin: 0; 7 + padding: 0; 8 + } 9 + 10 + /* Base Styles */ 11 + html { 12 + font-size: var(--spacing-base); 13 + line-height: var(--spacing-line-height); 14 + } 15 + 16 + /* Scoped to notebook-content container */ 17 + .notebook-content { 18 + font-family: var(--font-body); 19 + color: var(--color-text); 20 + background-color: var(--color-base); 21 + margin: 0 auto; 22 + padding: 1rem 0rem; 23 + word-wrap: break-word; 24 + overflow-wrap: break-word; 25 + counter-reset: sidenote-counter; 26 + max-width: 95ch; 27 + } 28 + 29 + /* When sidenotes exist, body padding creates the gutter */ 30 + /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 31 + body:has(.sidenote) { 32 + padding-inline-start: clamp(1rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 33 + padding-inline-end: 15.5rem; 34 + } 35 + 36 + /* Typography */ 37 + h1, 38 + h2, 39 + h3, 40 + h4, 41 + h5, 42 + h6 { 43 + font-family: var(--font-heading); 44 + margin-top: calc(1rem * var(--spacing-scale)); 45 + margin-bottom: 0.5rem; 46 + line-height: 1.2; 47 + } 48 + 49 + h1 { 50 + font-size: 2rem; 51 + color: var(--color-secondary); 52 + } 53 + h2 { 54 + font-size: 1.5rem; 55 + color: var(--color-primary); 56 + } 57 + h3 { 58 + font-size: 1.25rem; 59 + color: var(--color-secondary); 60 + } 61 + h4 { 62 + font-size: 1.2rem; 63 + color: var(--color-tertiary); 64 + } 65 + h5 { 66 + font-size: 1.125rem; 67 + color: var(--color-secondary); 68 + } 69 + h6 { 70 + font-size: 1rem; 71 + } 72 + 73 + p { 74 + margin-bottom: 1rem; 75 + word-wrap: break-word; 76 + overflow-wrap: break-word; 77 + } 78 + 79 + a { 80 + color: var(--color-link); 81 + text-decoration: none; 82 + } 83 + 84 + .notebook-content a:hover { 85 + color: var(--color-emphasis); 86 + text-decoration: underline; 87 + } 88 + 89 + /* Wikilink validation (editor) */ 90 + .link-valid { 91 + color: var(--color-link); 92 + } 93 + 94 + .link-broken { 95 + color: var(--color-error); 96 + text-decoration: underline wavy; 97 + text-decoration-color: var(--color-error); 98 + opacity: 0.8; 99 + } 100 + 101 + /* Selection */ 102 + ::selection { 103 + background: var(--color-highlight); 104 + color: var(--color-text); 105 + } 106 + 107 + /* Lists */ 108 + ul, 109 + ol { 110 + margin-inline-start: 1rem; 111 + margin-bottom: 1rem; 112 + } 113 + 114 + li { 115 + margin-bottom: 0.25rem; 116 + } 117 + 118 + /* Code */ 119 + code { 120 + font-family: var(--font-mono); 121 + background: var(--color-surface); 122 + padding: 0.125rem 0.25rem; 123 + border-radius: 4px; 124 + font-size: 0.9em; 125 + } 126 + 127 + pre { 128 + overflow-x: auto; 129 + margin-bottom: 1rem; 130 + border-radius: 5px; 131 + border: 1px solid var(--color-border); 132 + box-sizing: border-box; 133 + } 134 + 135 + /* Code blocks inside pre are handled by syntax theme */ 136 + pre code { 137 + display: block; 138 + width: fit-content; 139 + min-width: 100%; 140 + padding: 1rem; 141 + background: var(--color-surface); 142 + } 143 + 144 + /* Math */ 145 + .math { 146 + font-family: var(--font-mono); 147 + } 148 + 149 + .math-display { 150 + display: block; 151 + margin: 1rem 0; 152 + text-align: center; 153 + } 154 + 155 + /* Blockquotes */ 156 + blockquote { 157 + border-inline-start: 2px solid var(--color-secondary); 158 + background: var(--color-surface); 159 + padding-inline-start: 1rem; 160 + padding-inline-end: 1rem; 161 + padding-top: 0.5rem; 162 + padding-bottom: 0.04rem; 163 + margin: 1rem 0; 164 + font-size: 0.95em; 165 + border-bottom-right-radius: 5px; 166 + border-top-right-radius: 5px; 167 + } 168 + 169 + /* Tables */ 170 + table { 171 + border-collapse: collapse; 172 + width: 100%; 173 + margin-bottom: 1rem; 174 + display: block; 175 + overflow-x: auto; 176 + max-width: 100%; 177 + } 178 + 179 + th, 180 + td { 181 + border: 1px solid var(--color-border); 182 + padding: 0.5rem; 183 + text-align: start; 184 + } 185 + 186 + th { 187 + background: var(--color-surface); 188 + font-weight: 600; 189 + } 190 + 191 + tr:hover { 192 + background: var(--color-surface); 193 + } 194 + 195 + /* Footnotes */ 196 + .footnote-reference { 197 + font-size: 0.8em; 198 + color: var(--color-subtle); 199 + } 200 + 201 + .footnote-definition { 202 + order: 9999; 203 + margin: 0; 204 + padding: 0.5rem 0; 205 + font-size: 0.9em; 206 + } 207 + 208 + .footnote-definition:first-of-type { 209 + margin-top: 2rem; 210 + padding-top: 1rem; 211 + border-top: 2px solid var(--color-border); 212 + } 213 + 214 + .footnote-definition:first-of-type::before { 215 + content: "Footnotes"; 216 + display: block; 217 + font-weight: 600; 218 + font-size: 1.1em; 219 + color: var(--color-subtle); 220 + margin-bottom: 0.75rem; 221 + } 222 + 223 + .footnote-definition-label { 224 + font-weight: 600; 225 + margin-inline-end: 0.5rem; 226 + color: var(--color-primary); 227 + } 228 + 229 + /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 230 + .notebook-content aside, 231 + .notebook-content .aside { 232 + float: inline-start; 233 + width: 40%; 234 + margin: 0 1.5rem 1rem 0; 235 + padding: 1rem; 236 + background: var(--color-surface); 237 + border-inline-end: 3px solid var(--color-primary); 238 + font-size: 0.9em; 239 + clear: inline-start; 240 + } 241 + 242 + .notebook-content aside > *:first-child, 243 + .notebook-content .aside > *:first-child { 244 + margin-top: 0; 245 + } 246 + 247 + .notebook-content aside > *:last-child, 248 + .notebook-content .aside > *:last-child { 249 + margin-bottom: 0; 250 + } 251 + 252 + /* Reset blockquote styling inside asides */ 253 + .notebook-content aside > blockquote, 254 + .notebook-content .aside > blockquote { 255 + border-inline-start: none; 256 + background: transparent; 257 + padding: 0; 258 + margin: 0; 259 + font-size: inherit; 260 + } 261 + 262 + /* Indent utilities */ 263 + .indent-1 { 264 + margin-inline-start: 1em; 265 + } 266 + .indent-2 { 267 + margin-inline-start: 2em; 268 + } 269 + .indent-3 { 270 + margin-inline-start: 3em; 271 + } 272 + 273 + /* Tufte-style Sidenotes */ 274 + /* Hide checkbox for sidenote toggle */ 275 + .margin-toggle { 276 + display: none; 277 + } 278 + 279 + /* Sidenote number marker (inline superscript) */ 280 + .sidenote-number { 281 + counter-increment: sidenote-counter; 282 + } 283 + 284 + .sidenote-number::after { 285 + content: counter(sidenote-counter); 286 + font-size: 0.7em; 287 + position: relative; 288 + top: -0.5em; 289 + color: var(--color-primary); 290 + padding-inline-start: 0.1em; 291 + } 292 + 293 + /* Sidenote content (margin notes on wide screens) */ 294 + .sidenote { 295 + float: inline-end; 296 + clear: inline-end; 297 + margin-inline-end: -15.5rem; 298 + width: 14rem; 299 + margin-top: 0.3rem; 300 + margin-bottom: 1rem; 301 + font-size: 0.85em; 302 + line-height: 1.4; 303 + color: var(--color-subtle); 304 + } 305 + 306 + .sidenote::before { 307 + content: counter(sidenote-counter) ". "; 308 + color: var(--color-primary); 309 + } 310 + 311 + /* Mobile sidenotes: toggle behavior */ 312 + @media (max-width: 900px) { 313 + /* Reset sidenote gutter on mobile */ 314 + body:has(.sidenote) { 315 + padding-inline-end: 0; 316 + } 317 + 318 + aside, 319 + .aside { 320 + float: none; 321 + width: 100%; 322 + margin: 1rem 0; 323 + } 324 + 325 + .sidenote { 326 + display: none; 327 + } 328 + 329 + .margin-toggle:checked + .sidenote { 330 + display: block; 331 + float: none; 332 + width: 95%; 333 + margin: 0.5rem 2.5%; 334 + padding: 0.5rem; 335 + background: var(--color-surface); 336 + border-inline-start: 2px solid var(--color-primary); 337 + } 338 + 339 + label.sidenote-number { 340 + cursor: pointer; 341 + } 342 + 343 + label.sidenote-number::after { 344 + text-decoration: underline; 345 + } 346 + } 347 + 348 + /* Images */ 349 + img { 350 + max-width: 100%; 351 + height: auto; 352 + display: block; 353 + margin: 1rem 0; 354 + border-radius: 4px; 355 + } 356 + 357 + /* Hygiene for iframes */ 358 + .html-embed-block { 359 + max-width: 100%; 360 + height: auto; 361 + display: block; 362 + margin: 1rem 0; 363 + } 364 + 365 + /* AT Protocol Embeds - Container */ 366 + /* Light mode: paper with shadow, dark mode: blueprint with borders */ 367 + .atproto-embed { 368 + display: block; 369 + position: relative; 370 + max-width: 550px; 371 + margin: 1rem 0; 372 + padding: 1rem; 373 + background: var(--color-surface); 374 + border-inline-start: 2px solid var(--color-secondary); 375 + box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 376 + } 377 + 378 + .atproto-embed:hover { 379 + border-inline-start-color: var(--color-primary); 380 + } 381 + 382 + @media (prefers-color-scheme: dark) { 383 + .atproto-embed { 384 + box-shadow: none; 385 + border: 1px solid var(--color-border); 386 + border-inline-start: 2px solid var(--color-secondary); 387 + } 388 + } 389 + 390 + .atproto-embed-placeholder { 391 + color: var(--color-muted); 392 + font-style: italic; 393 + } 394 + 395 + .embed-loading { 396 + display: block; 397 + padding: 0.5rem 0; 398 + color: var(--color-subtle); 399 + font-family: var(--font-mono); 400 + font-size: 0.85rem; 401 + } 402 + 403 + /* Embed Author Block */ 404 + .embed-author { 405 + display: flex; 406 + align-items: center; 407 + gap: 0.75rem; 408 + padding-bottom: 0.5rem; 409 + } 410 + 411 + .embed-avatar { 412 + width: 36px; 413 + height: 36px; 414 + max-width: 36px; 415 + max-height: 36px; 416 + aspect-ratio: 1; 417 + margin: 0; 418 + object-fit: cover; 419 + } 420 + 421 + .embed-author-info { 422 + display: flex; 423 + flex-direction: column; 424 + gap: 0; 425 + min-width: 0; 426 + } 427 + 428 + .embed-avatar-link { 429 + display: block; 430 + flex-shrink: 0; 431 + } 432 + 433 + .embed-author-name { 434 + font-weight: 600; 435 + color: var(--color-text); 436 + overflow: hidden; 437 + text-overflow: ellipsis; 438 + white-space: nowrap; 439 + text-decoration: none; 440 + line-height: 1.2; 441 + } 442 + 443 + a.embed-author-name:hover { 444 + color: var(--color-link); 445 + } 446 + 447 + .embed-author-handle { 448 + font-size: 0.85em; 449 + font-family: var(--font-mono); 450 + color: var(--color-subtle); 451 + text-decoration: none; 452 + overflow: hidden; 453 + text-overflow: ellipsis; 454 + white-space: nowrap; 455 + line-height: 1.2; 456 + } 457 + 458 + .embed-author-handle:hover { 459 + color: var(--color-link); 460 + } 461 + 462 + /* Card-wide clickable link (sits behind content) */ 463 + .embed-card-link { 464 + position: absolute; 465 + inset: 0; 466 + z-index: 0; 467 + } 468 + 469 + .embed-card-link:focus { 470 + outline: 2px solid var(--color-primary); 471 + outline-offset: 2px; 472 + } 473 + 474 + /* Interactive elements sit above the card link */ 475 + .embed-author, 476 + .embed-external, 477 + .embed-quote, 478 + .embed-images, 479 + .embed-meta { 480 + position: relative; 481 + z-index: 1; 482 + } 483 + 484 + /* Embed Content Block */ 485 + .embed-content { 486 + display: block; 487 + color: var(--color-text); 488 + line-height: 1.5; 489 + margin-bottom: 0.75rem; 490 + white-space: pre-wrap; 491 + } 492 + 493 + .embed-description { 494 + display: block; 495 + color: var(--color-text); 496 + font-size: 0.95em; 497 + line-height: 1.4; 498 + } 499 + 500 + /* Embed Metadata Block */ 501 + .embed-meta { 502 + display: flex; 503 + justify-content: space-between; 504 + align-items: center; 505 + font-size: 0.85em; 506 + color: var(--color-muted); 507 + margin-top: 0.75rem; 508 + } 509 + 510 + .embed-stats { 511 + display: flex; 512 + gap: 1rem; 513 + font-family: var(--font-mono); 514 + } 515 + 516 + .embed-stat { 517 + color: var(--color-subtle); 518 + font-size: 0.9em; 519 + } 520 + 521 + .embed-time { 522 + color: var(--color-subtle); 523 + text-decoration: none; 524 + font-family: var(--font-mono); 525 + font-size: 0.9em; 526 + } 527 + 528 + .embed-time:hover { 529 + color: var(--color-link); 530 + } 531 + 532 + .embed-type { 533 + font-size: 0.8em; 534 + color: var(--color-subtle); 535 + font-family: var(--font-mono); 536 + text-transform: uppercase; 537 + letter-spacing: 0.05em; 538 + } 539 + 540 + /* Embed URL link (shown with syntax in editor) */ 541 + .embed-url { 542 + color: var(--color-link); 543 + font-family: var(--font-mono); 544 + font-size: 0.9em; 545 + word-break: break-all; 546 + } 547 + 548 + /* External link cards */ 549 + .embed-external { 550 + display: flex; 551 + gap: 0.75rem; 552 + padding: 0.75rem; 553 + background: var(--color-surface); 554 + border: 1px dashed var(--color-border); 555 + text-decoration: none; 556 + color: inherit; 557 + margin-top: 0.5rem; 558 + } 559 + 560 + .embed-external:hover { 561 + border-inline-start: 2px solid var(--color-primary); 562 + margin-inline-start: -1px; 563 + } 564 + 565 + @media (prefers-color-scheme: dark) { 566 + .embed-external { 567 + border: 1px solid var(--color-border); 568 + } 569 + 570 + .embed-external:hover { 571 + border-inline-start: 2px solid var(--color-primary); 572 + margin-inline-start: -1px; 573 + } 574 + } 575 + 576 + .embed-external-thumb { 577 + width: 120px; 578 + height: 80px; 579 + object-fit: cover; 580 + flex-shrink: 0; 581 + } 582 + 583 + .embed-external-info { 584 + display: flex; 585 + flex-direction: column; 586 + gap: 0.25rem; 587 + min-width: 0; 588 + } 589 + 590 + .embed-external-title { 591 + font-weight: 600; 592 + color: var(--color-text); 593 + overflow: hidden; 594 + text-overflow: ellipsis; 595 + white-space: nowrap; 596 + } 597 + 598 + .embed-external-description { 599 + font-size: 0.9em; 600 + color: var(--color-muted); 601 + overflow: hidden; 602 + text-overflow: ellipsis; 603 + display: -webkit-box; 604 + -webkit-line-clamp: 2; 605 + -webkit-box-orient: vertical; 606 + } 607 + 608 + .embed-external-url { 609 + font-size: 0.8em; 610 + font-family: var(--font-mono); 611 + color: var(--color-subtle); 612 + } 613 + 614 + /* Image embeds */ 615 + .embed-images { 616 + display: grid; 617 + gap: 4px; 618 + margin-top: 0.5rem; 619 + overflow: hidden; 620 + } 621 + 622 + .embed-images-1 { 623 + grid-template-columns: 1fr; 624 + } 625 + 626 + .embed-images-2 { 627 + grid-template-columns: 1fr 1fr; 628 + } 629 + 630 + .embed-images-3 { 631 + grid-template-columns: 1fr 1fr; 632 + } 633 + 634 + .embed-images-4 { 635 + grid-template-columns: 1fr 1fr; 636 + } 637 + 638 + .embed-image-link { 639 + display: block; 640 + line-height: 0; 641 + } 642 + 643 + .embed-image { 644 + width: 100%; 645 + height: auto; 646 + max-height: 500px; 647 + object-fit: cover; 648 + object-position: center; 649 + margin: 0; 650 + } 651 + 652 + /* Quoted records */ 653 + .embed-quote { 654 + display: block; 655 + margin-top: 0.5rem; 656 + padding: 0.75rem; 657 + background: var(--color-overlay); 658 + border-inline-start: 2px solid var(--color-tertiary); 659 + } 660 + 661 + @media (prefers-color-scheme: dark) { 662 + .embed-quote { 663 + border: 1px solid var(--color-border); 664 + border-inline-start: 2px solid var(--color-tertiary); 665 + } 666 + } 667 + 668 + .embed-quote .embed-author { 669 + margin-bottom: 0.5rem; 670 + } 671 + 672 + .embed-quote .embed-avatar { 673 + width: 24px; 674 + height: 24px; 675 + min-width: 24px; 676 + min-height: 24px; 677 + max-width: 24px; 678 + max-height: 24px; 679 + } 680 + 681 + .embed-quote .embed-content { 682 + font-size: 0.95em; 683 + margin-bottom: 0; 684 + } 685 + 686 + /* Placeholder states */ 687 + .embed-video-placeholder, 688 + .embed-not-found, 689 + .embed-blocked, 690 + .embed-detached, 691 + .embed-unknown { 692 + display: block; 693 + padding: 1rem; 694 + background: var(--color-overlay); 695 + border-inline-start: 2px solid var(--color-border); 696 + color: var(--color-muted); 697 + font-style: italic; 698 + margin-top: 0.5rem; 699 + font-family: var(--font-mono); 700 + font-size: 0.9em; 701 + } 702 + 703 + @media (prefers-color-scheme: dark) { 704 + .embed-video-placeholder, 705 + .embed-not-found, 706 + .embed-blocked, 707 + .embed-detached, 708 + .embed-unknown { 709 + border: 1px dashed var(--color-border); 710 + } 711 + } 712 + 713 + /* Record card embeds (feeds, lists, labelers, starter packs) */ 714 + .embed-record-card { 715 + display: block; 716 + margin-top: 0.5rem; 717 + padding: 0.75rem; 718 + background: var(--color-overlay); 719 + border-inline-start: 2px solid var(--color-tertiary); 720 + } 721 + 722 + .embed-record-card > .embed-author-name { 723 + display: block; 724 + font-size: 1.1em; 725 + } 726 + 727 + .embed-subtitle { 728 + display: block; 729 + font-size: 0.85em; 730 + color: var(--color-muted); 731 + margin-bottom: 0.5rem; 732 + } 733 + 734 + .embed-record-card .embed-description { 735 + display: block; 736 + margin: 0.5rem 0; 737 + } 738 + 739 + .embed-record-card .embed-stats { 740 + display: block; 741 + margin-top: 0.25rem; 742 + } 743 + 744 + /* Generic record fields */ 745 + .embed-fields { 746 + display: block; 747 + margin-top: 0.5rem; 748 + font-family: var(--font-ui); 749 + font-size: 0.85rem; 750 + color: var(--color-muted); 751 + } 752 + 753 + .embed-field { 754 + display: block; 755 + margin-top: 0.25rem; 756 + } 757 + 758 + /* Nested fields get indentation */ 759 + .embed-fields .embed-fields { 760 + display: block; 761 + margin-top: 0.5rem; 762 + margin-inline-start: 1rem; 763 + padding-inline-start: 0.5rem; 764 + border-inline-start: 1px solid var(--color-border); 765 + } 766 + 767 + /* Type label inside fields should be block with spacing */ 768 + .embed-fields > .embed-author-handle { 769 + display: block; 770 + margin-bottom: 0.25rem; 771 + } 772 + 773 + .embed-field-name { 774 + color: var(--color-subtle); 775 + } 776 + 777 + .embed-field-number { 778 + color: var(--color-tertiary); 779 + } 780 + 781 + .embed-field-date { 782 + color: var(--color-muted); 783 + } 784 + 785 + .embed-field-count { 786 + color: var(--color-muted); 787 + font-style: italic; 788 + } 789 + 790 + .embed-field-bool-true { 791 + color: var(--color-success); 792 + } 793 + 794 + .embed-field-bool-false { 795 + color: var(--color-muted); 796 + } 797 + 798 + .embed-field-link, 799 + .embed-field-aturi { 800 + color: var(--color-link); 801 + text-decoration: none; 802 + } 803 + 804 + .embed-field-link:hover, 805 + .embed-field-aturi:hover { 806 + text-decoration: underline; 807 + } 808 + 809 + .embed-field-did { 810 + font-family: var(--font-mono); 811 + font-size: 0.9em; 812 + } 813 + 814 + .embed-field-did .did-scheme, 815 + .embed-field-did .did-separator { 816 + color: var(--color-muted); 817 + } 818 + 819 + .embed-field-did .did-method { 820 + color: var(--color-tertiary); 821 + } 822 + 823 + .embed-field-did .did-identifier { 824 + color: var(--color-text); 825 + } 826 + 827 + .embed-field-nsid { 828 + color: var(--color-secondary); 829 + } 830 + 831 + .embed-field-handle { 832 + color: var(--color-link); 833 + } 834 + 835 + /* AT URI highlighting */ 836 + .aturi-scheme { 837 + color: var(--color-muted); 838 + } 839 + 840 + .aturi-slash { 841 + color: var(--color-muted); 842 + } 843 + 844 + .aturi-authority { 845 + color: var(--color-link); 846 + } 847 + 848 + .aturi-collection { 849 + color: var(--color-secondary); 850 + } 851 + 852 + .aturi-rkey { 853 + color: var(--color-tertiary); 854 + } 855 + 856 + /* Generic AT Protocol record embed */ 857 + .atproto-record > .embed-author-handle { 858 + display: block; 859 + margin-bottom: 0.25rem; 860 + } 861 + 862 + .atproto-record > .embed-author-name { 863 + display: block; 864 + margin-bottom: 0.5rem; 865 + } 866 + 867 + .atproto-record > .embed-content { 868 + margin-bottom: 0.5rem; 869 + } 870 + 871 + /* Notebook entry embed - full width, expandable */ 872 + .atproto-entry { 873 + max-width: none; 874 + width: 100%; 875 + margin: 1.5rem 0; 876 + padding: 0; 877 + background: var(--color-surface); 878 + border: 1px solid var(--color-border); 879 + border-inline-start: 1px solid var(--color-border); 880 + box-shadow: none; 881 + overflow: hidden; 882 + } 883 + 884 + .atproto-entry:hover { 885 + border-inline-start-color: var(--color-border); 886 + } 887 + 888 + @media (prefers-color-scheme: dark) { 889 + .atproto-entry { 890 + border: 1px solid var(--color-border); 891 + border-inline-start: 1px solid var(--color-border); 892 + } 893 + } 894 + 895 + .embed-entry-header { 896 + display: flex; 897 + flex-wrap: wrap; 898 + align-items: baseline; 899 + gap: 0.5rem 1rem; 900 + padding: 0.75rem 1rem; 901 + background: var(--color-overlay); 902 + border-bottom: 1px solid var(--color-border); 903 + } 904 + 905 + .embed-entry-title { 906 + font-size: 1.1em; 907 + font-weight: 600; 908 + color: var(--color-text); 909 + } 910 + 911 + .embed-entry-author { 912 + font-size: 0.85em; 913 + color: var(--color-muted); 914 + } 915 + 916 + /* Hidden checkbox for expand/collapse */ 917 + .embed-entry-toggle { 918 + display: none; 919 + } 920 + 921 + /* Content wrapper - scrollable when collapsed */ 922 + .embed-entry-content { 923 + max-height: 30rem; 924 + overflow-y: auto; 925 + padding: 1rem; 926 + transition: max-height 0.3s ease; 927 + } 928 + 929 + /* When checkbox is checked, expand fully */ 930 + .embed-entry-toggle:checked ~ .embed-entry-content { 931 + max-height: none; 932 + } 933 + 934 + /* Expand/collapse button */ 935 + .embed-entry-expand { 936 + display: block; 937 + width: 100%; 938 + padding: 0.5rem; 939 + text-align: center; 940 + font-size: 0.85em; 941 + font-family: var(--font-ui); 942 + color: var(--color-muted); 943 + background: var(--color-overlay); 944 + border-top: 1px solid var(--color-border); 945 + cursor: pointer; 946 + user-select: none; 947 + } 948 + 949 + .embed-entry-expand:hover { 950 + color: var(--color-text); 951 + background: var(--color-surface); 952 + } 953 + 954 + /* Toggle button text */ 955 + .embed-entry-expand::before { 956 + content: "Expand ↓"; 957 + } 958 + 959 + .embed-entry-toggle:checked ~ .embed-entry-expand::before { 960 + content: "Collapse ↑"; 961 + } 962 + 963 + /* Hide expand button if content doesn't overflow (via JS class) */ 964 + .atproto-entry.no-overflow .embed-entry-expand { 965 + display: none; 966 + } 967 + 968 + /* Horizontal Rule */ 969 + hr { 970 + border: none; 971 + border-top: 2px solid var(--color-border); 972 + margin: 2rem 0; 973 + } 974 + 975 + /* Tablet and mobile responsiveness */ 976 + @media (max-width: 900px) { 977 + .notebook-content { 978 + padding: 1.5rem 1rem; 979 + max-width: 100%; 980 + } 981 + 982 + h1 { 983 + font-size: 1.85rem; 984 + } 985 + h2 { 986 + font-size: 1.4rem; 987 + } 988 + h3 { 989 + font-size: 1.2rem; 990 + } 991 + 992 + blockquote { 993 + margin-inline-start: 0; 994 + margin-inline-end: 0; 995 + } 996 + } 997 + 998 + /* Small mobile phones */ 999 + @media (max-width: 480px) { 1000 + .notebook-content { 1001 + padding: 1rem 0.75rem; 1002 + } 1003 + 1004 + h1 { 1005 + font-size: 1.65rem; 1006 + } 1007 + h2 { 1008 + font-size: 1.3rem; 1009 + } 1010 + h3 { 1011 + font-size: 1.1rem; 1012 + } 1013 + 1014 + blockquote { 1015 + padding-inline-start: 0.75rem; 1016 + padding-inline-end: 0.75rem; 1017 + } 1018 + } 1019 + 1020 + /* Leaflet document embeds */ 1021 + .atproto-leaflet { 1022 + max-width: none; 1023 + width: 100%; 1024 + margin: 1rem 0; 1025 + } 1026 + 1027 + .leaflet-document { 1028 + display: block; 1029 + } 1030 + 1031 + .leaflet-text { 1032 + margin: 0.5rem 0; 1033 + } 1034 + 1035 + .leaflet-button { 1036 + display: inline-block; 1037 + padding: 0.5rem 1rem; 1038 + background: var(--color-primary); 1039 + color: var(--color-base); 1040 + text-decoration: none; 1041 + border-radius: 4px; 1042 + margin: 0.5rem 0; 1043 + } 1044 + 1045 + .leaflet-button:hover { 1046 + opacity: 0.9; 1047 + } 1048 + 1049 + /* Alignment utilities */ 1050 + .align-center { 1051 + text-align: center; 1052 + } 1053 + .align-right { 1054 + text-align: right; 1055 + } 1056 + .align-justify { 1057 + text-align: justify; 1058 + } 1059 + 1060 + /* ========================================================================== 1061 + SYNTAX HIGHLIGHTING 1062 + ========================================================================== */ 1063 + 1064 + /* Syntax highlighting - Light Mode (default) */ 1065 + /* 1066 + * theme "Rosé Pine Dawn" generated by syntect 1067 + */ 1068 + 1069 + .wvc-code { 1070 + color: #575279; 1071 + background-color: #faf4ed; 1072 + } 1073 + 1074 + .wvc-comment { 1075 + color: #797593; 1076 + font-style: italic; 1077 + } 1078 + .wvc-string, 1079 + .wvc-punctuation.wvc-definition.wvc-string { 1080 + color: #ea9d34; 1081 + } 1082 + .wvc-constant.wvc-numeric { 1083 + color: #ea9d34; 1084 + } 1085 + .wvc-constant.wvc-language { 1086 + color: #ea9d34; 1087 + font-weight: bold; 1088 + } 1089 + .wvc-constant.wvc-character, 1090 + .wvc-constant.wvc-other { 1091 + color: #ea9d34; 1092 + } 1093 + .wvc-variable { 1094 + color: #575279; 1095 + font-style: italic; 1096 + } 1097 + .wvc-keyword { 1098 + color: #286983; 1099 + } 1100 + .wvc-storage { 1101 + color: #56949f; 1102 + } 1103 + .wvc-storage.wvc-type { 1104 + color: #56949f; 1105 + } 1106 + .wvc-entity.wvc-name.wvc-class { 1107 + color: #286983; 1108 + font-weight: bold; 1109 + } 1110 + .wvc-entity.wvc-other.wvc-inherited-class { 1111 + color: #286983; 1112 + font-style: italic; 1113 + } 1114 + .wvc-entity.wvc-name.wvc-function { 1115 + color: #d7827e; 1116 + font-style: italic; 1117 + } 1118 + .wvc-variable.wvc-parameter { 1119 + color: #907aa9; 1120 + } 1121 + .wvc-entity.wvc-name.wvc-tag { 1122 + color: #286983; 1123 + font-weight: bold; 1124 + } 1125 + .wvc-entity.wvc-other.wvc-attribute-name { 1126 + color: #907aa9; 1127 + } 1128 + .wvc-support.wvc-function { 1129 + color: #d7827e; 1130 + font-weight: bold; 1131 + } 1132 + .wvc-support.wvc-constant { 1133 + color: #ea9d34; 1134 + font-weight: bold; 1135 + } 1136 + .wvc-support.wvc-type, 1137 + .wvc-support.wvc-class { 1138 + color: #56949f; 1139 + font-weight: bold; 1140 + } 1141 + .wvc-support.wvc-other.wvc-variable { 1142 + color: #b4637a; 1143 + font-weight: bold; 1144 + } 1145 + .wvc-invalid { 1146 + color: #575279; 1147 + background-color: #b4637a; 1148 + } 1149 + .wvc-invalid.wvc-deprecated { 1150 + color: #575279; 1151 + background-color: #907aa9; 1152 + } 1153 + .wvc-punctuation, 1154 + .wvc-keyword.wvc-operator { 1155 + color: #797593; 1156 + } 1157 + 1158 + /* Syntax highlighting - Dark Mode */ 1159 + @media (prefers-color-scheme: dark) { 1160 + /* 1161 + * theme "Rosé Pine" generated by syntect 1162 + */ 1163 + 1164 + .wvc-code { 1165 + color: #e0def4; 1166 + background-color: #191724; 1167 + } 1168 + 1169 + .wvc-comment { 1170 + color: #908caa; 1171 + font-style: italic; 1172 + } 1173 + .wvc-string, 1174 + .wvc-punctuation.wvc-definition.wvc-string { 1175 + color: #f6c177; 1176 + } 1177 + .wvc-constant.wvc-numeric { 1178 + color: #f6c177; 1179 + } 1180 + .wvc-constant.wvc-language { 1181 + color: #f6c177; 1182 + font-weight: bold; 1183 + } 1184 + .wvc-constant.wvc-character, 1185 + .wvc-constant.wvc-other { 1186 + color: #f6c177; 1187 + } 1188 + .wvc-variable { 1189 + color: #e0def4; 1190 + font-style: italic; 1191 + } 1192 + .wvc-keyword { 1193 + color: #31748f; 1194 + } 1195 + .wvc-storage { 1196 + color: #9ccfd8; 1197 + } 1198 + .wvc-storage.wvc-type { 1199 + color: #9ccfd8; 1200 + } 1201 + .wvc-entity.wvc-name.wvc-class { 1202 + color: #31748f; 1203 + font-weight: bold; 1204 + } 1205 + .wvc-entity.wvc-other.wvc-inherited-class { 1206 + color: #31748f; 1207 + font-style: italic; 1208 + } 1209 + .wvc-entity.wvc-name.wvc-function { 1210 + color: #ebbcba; 1211 + font-style: italic; 1212 + } 1213 + .wvc-variable.wvc-parameter { 1214 + color: #c4a7e7; 1215 + } 1216 + .wvc-entity.wvc-name.wvc-tag { 1217 + color: #31748f; 1218 + font-weight: bold; 1219 + } 1220 + .wvc-entity.wvc-other.wvc-attribute-name { 1221 + color: #c4a7e7; 1222 + } 1223 + .wvc-support.wvc-function { 1224 + color: #ebbcba; 1225 + font-weight: bold; 1226 + } 1227 + .wvc-support.wvc-constant { 1228 + color: #f6c177; 1229 + font-weight: bold; 1230 + } 1231 + .wvc-support.wvc-type, 1232 + .wvc-support.wvc-class { 1233 + color: #9ccfd8; 1234 + font-weight: bold; 1235 + } 1236 + .wvc-support.wvc-other.wvc-variable { 1237 + color: #eb6f92; 1238 + font-weight: bold; 1239 + } 1240 + .wvc-invalid { 1241 + color: #e0def4; 1242 + background-color: #eb6f92; 1243 + } 1244 + .wvc-invalid.wvc-deprecated { 1245 + color: #e0def4; 1246 + background-color: #c4a7e7; 1247 + } 1248 + .wvc-punctuation, 1249 + .wvc-keyword.wvc-operator { 1250 + color: #908caa; 1251 + } 1252 + }
+35 -19
crates/weaver-app/src/components/css.rs
··· 88 89 #[component] 90 pub fn DefaultNotebookCss() -> Element { 91 - use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 92 - use weaver_renderer::theme::default_resolved_theme; 93 - 94 - let css_content = use_resource(move || async move { 95 - let resolved_theme = default_resolved_theme(); 96 - 97 - let mut css = generate_base_css(&resolved_theme); 98 - css.push_str( 99 - &generate_syntax_css(&resolved_theme) 100 - .await 101 - .unwrap_or_default(), 102 - ); 103 - 104 - Some(css) 105 - }); 106 - 107 - match css_content() { 108 - Some(Some(css)) => rsx! { document::Style { {css} } }, 109 - _ => rsx! {}, 110 } 111 } 112 ··· 155 .unwrap_or_default(), 156 ); 157 158 Ok(([(CONTENT_TYPE, "text/css")], css).into_response()) 159 }
··· 88 89 #[component] 90 pub fn DefaultNotebookCss() -> Element { 91 + rsx! { 92 + document::Stylesheet { href: asset!("/assets/styling/theme-defaults.css") } 93 + document::Stylesheet { href: asset!("/assets/styling/notebook-defaults.css") } 94 } 95 } 96 ··· 139 .unwrap_or_default(), 140 ); 141 142 + let css = minify_css(&css).unwrap_or(css); 143 + 144 Ok(([(CONTENT_TYPE, "text/css")], css).into_response()) 145 } 146 + 147 + #[cfg(feature = "server")] 148 + fn minify_css(css: &str) -> Option<String> { 149 + use lightningcss::printer::PrinterOptions; 150 + use lightningcss::stylesheet::{MinifyOptions, ParserOptions, StyleSheet}; 151 + 152 + let stylesheet = match StyleSheet::parse(css, ParserOptions::default()) { 153 + Ok(s) => s, 154 + Err(e) => { 155 + tracing::warn!("CSS parse error: {:?}", e); 156 + return None; 157 + } 158 + }; 159 + let mut stylesheet = stylesheet; 160 + if let Err(e) = stylesheet.minify(MinifyOptions::default()) { 161 + tracing::warn!("CSS minify error: {:?}", e); 162 + return None; 163 + } 164 + let result = match stylesheet.to_css(PrinterOptions { 165 + minify: true, 166 + ..Default::default() 167 + }) { 168 + Ok(r) => r, 169 + Err(e) => { 170 + tracing::warn!("CSS print error: {:?}", e); 171 + return None; 172 + } 173 + }; 174 + Some(result.code) 175 + }
+98 -5
crates/weaver-app/src/data.rs
··· 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 rkey: ReadSignal<SmolStr>, 1587 ) -> ( 1588 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>)>>, RenderError>, 1589 Memo<Option<crate::fetch::LeafletDocumentData>>, 1590 ) { 1591 use weaver_api::pub_leaflet::document::Document; ··· 1595 let res = use_server_future(move || { 1596 let fetcher = fetcher.clone(); 1597 async move { 1598 use jacquard::client::AgentSessionExt; 1599 use weaver_api::pub_leaflet::publication::Publication; 1600 1601 let ident = ident(); 1602 let rkey = rkey(); ··· 1646 None 1647 }; 1648 1649 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1650 1651 Some(( 1652 serde_json::to_value(&record.value).ok()?, 1653 serde_json::to_value(&*profile).ok()?, 1654 publication_base_path, 1655 )) 1656 } 1657 }); ··· 1661 use weaver_api::sh_weaver::actor::ProfileDataView; 1662 1663 let res = res.as_ref().ok()?; 1664 - if let Some(Some((doc_json, profile_json, base_path))) = &*res.read() { 1665 let document = jacquard::from_json_value::<Document>(doc_json.clone()).ok()?; 1666 let profile = 1667 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; ··· 1669 document, 1670 profile, 1671 publication_base_path: base_path.clone(), 1672 }) 1673 } else { 1674 None ··· 1687 Memo<Option<crate::fetch::LeafletDocumentData>>, 1688 ) { 1689 use jacquard::IntoStatic; 1690 - use weaver_api::pub_leaflet::document::Document; 1691 use weaver_api::pub_leaflet::publication::Publication; 1692 1693 let fetcher = use_context::<crate::fetch::Fetcher>(); 1694 ··· 1730 None 1731 }; 1732 1733 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1734 1735 Some(crate::fetch::LeafletDocumentData { 1736 document: record.value.into_static(), 1737 profile: (*profile).clone(), 1738 publication_base_path, 1739 }) 1740 } 1741 }); ··· 1753 ident: ReadSignal<AtIdentifier<'static>>, 1754 rkey: ReadSignal<SmolStr>, 1755 ) -> ( 1756 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>)>>, RenderError>, 1757 Memo<Option<crate::fetch::PcktDocumentData>>, 1758 ) { 1759 let fetcher = use_context::<crate::fetch::Fetcher>(); ··· 1761 let res = use_server_future(move || { 1762 let fetcher = fetcher.clone(); 1763 async move { 1764 use jacquard::client::AgentSessionExt; 1765 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1766 use weaver_api::site_standard::publication::Publication; 1767 1768 let ident = ident(); 1769 let rkey = rkey(); ··· 1807 None 1808 }; 1809 1810 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1811 1812 Some(( 1813 serde_json::to_value(&doc).ok()?, 1814 serde_json::to_value(&*profile).ok()?, 1815 publication_url, 1816 )) 1817 } 1818 }); ··· 1822 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1823 1824 let res = res.as_ref().ok()?; 1825 - if let Some(Some((doc_json, profile_json, publication_url))) = &*res.read() { 1826 let document = 1827 jacquard::from_json_value::<SiteStandardDocument>(doc_json.clone()).ok()?; 1828 let profile = ··· 1831 document, 1832 profile, 1833 publication_url: publication_url.clone(), 1834 }) 1835 } else { 1836 None ··· 1849 Memo<Option<crate::fetch::PcktDocumentData>>, 1850 ) { 1851 use jacquard::IntoStatic; 1852 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1853 use weaver_api::site_standard::publication::Publication; 1854 1855 let fetcher = use_context::<crate::fetch::Fetcher>(); 1856 ··· 1899 None 1900 }; 1901 1902 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1903 1904 Some(crate::fetch::PcktDocumentData { 1905 document: doc, 1906 profile: (*profile).clone(), 1907 publication_url, 1908 }) 1909 } 1910 });
··· 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 rkey: ReadSignal<SmolStr>, 1587 ) -> ( 1588 + Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1589 Memo<Option<crate::fetch::LeafletDocumentData>>, 1590 ) { 1591 use weaver_api::pub_leaflet::document::Document; ··· 1595 let res = use_server_future(move || { 1596 let fetcher = fetcher.clone(); 1597 async move { 1598 + use jacquard::IntoStatic; 1599 use jacquard::client::AgentSessionExt; 1600 + use jacquard::prelude::IdentityResolver; 1601 + use weaver_api::pub_leaflet::document::DocumentPagesItem; 1602 use weaver_api::pub_leaflet::publication::Publication; 1603 + use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1604 1605 let ident = ident(); 1606 let rkey = rkey(); ··· 1650 None 1651 }; 1652 1653 + // Render HTML 1654 + let rendered_html = { 1655 + let author_did = match &record.value.author { 1656 + AtIdentifier::Did(d) => d.clone().into_static(), 1657 + AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1658 + }; 1659 + let ctx = LeafletRenderContext::new(author_did); 1660 + let mut html = String::new(); 1661 + for page in &record.value.pages { 1662 + match page { 1663 + DocumentPagesItem::LinearDocument(linear_doc) => { 1664 + html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1665 + } 1666 + DocumentPagesItem::Canvas(_) => { 1667 + html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1668 + } 1669 + DocumentPagesItem::Unknown(_) => { 1670 + html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1671 + } 1672 + } 1673 + } 1674 + Some(html) 1675 + }; 1676 + 1677 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1678 1679 Some(( 1680 serde_json::to_value(&record.value).ok()?, 1681 serde_json::to_value(&*profile).ok()?, 1682 publication_base_path, 1683 + rendered_html, 1684 )) 1685 } 1686 }); ··· 1690 use weaver_api::sh_weaver::actor::ProfileDataView; 1691 1692 let res = res.as_ref().ok()?; 1693 + if let Some(Some((doc_json, profile_json, base_path, rendered_html))) = &*res.read() { 1694 let document = jacquard::from_json_value::<Document>(doc_json.clone()).ok()?; 1695 let profile = 1696 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; ··· 1698 document, 1699 profile, 1700 publication_base_path: base_path.clone(), 1701 + rendered_html: rendered_html.clone(), 1702 }) 1703 } else { 1704 None ··· 1717 Memo<Option<crate::fetch::LeafletDocumentData>>, 1718 ) { 1719 use jacquard::IntoStatic; 1720 + use jacquard::prelude::IdentityResolver; 1721 + use weaver_api::pub_leaflet::document::{Document, DocumentPagesItem}; 1722 use weaver_api::pub_leaflet::publication::Publication; 1723 + use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1724 1725 let fetcher = use_context::<crate::fetch::Fetcher>(); 1726 ··· 1762 None 1763 }; 1764 1765 + // Render HTML 1766 + let rendered_html = { 1767 + let author_did = match &record.value.author { 1768 + AtIdentifier::Did(d) => d.clone().into_static(), 1769 + AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1770 + }; 1771 + let ctx = LeafletRenderContext::new(author_did); 1772 + let mut html = String::new(); 1773 + for page in &record.value.pages { 1774 + match page { 1775 + DocumentPagesItem::LinearDocument(linear_doc) => { 1776 + html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 1777 + } 1778 + DocumentPagesItem::Canvas(_) => { 1779 + html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 1780 + } 1781 + DocumentPagesItem::Unknown(_) => { 1782 + html.push_str("<div class=\"embed-video-placeholder\">[Unknown page type]</div>"); 1783 + } 1784 + } 1785 + } 1786 + Some(html) 1787 + }; 1788 + 1789 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1790 1791 Some(crate::fetch::LeafletDocumentData { 1792 document: record.value.into_static(), 1793 profile: (*profile).clone(), 1794 publication_base_path, 1795 + rendered_html, 1796 }) 1797 } 1798 }); ··· 1810 ident: ReadSignal<AtIdentifier<'static>>, 1811 rkey: ReadSignal<SmolStr>, 1812 ) -> ( 1813 + Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1814 Memo<Option<crate::fetch::PcktDocumentData>>, 1815 ) { 1816 let fetcher = use_context::<crate::fetch::Fetcher>(); ··· 1818 let res = use_server_future(move || { 1819 let fetcher = fetcher.clone(); 1820 async move { 1821 + use jacquard::IntoStatic; 1822 use jacquard::client::AgentSessionExt; 1823 + use jacquard::prelude::IdentityResolver; 1824 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1825 use weaver_api::site_standard::publication::Publication; 1826 + use weaver_renderer::pckt::{PcktRenderContext, render_content_blocks}; 1827 1828 let ident = ident(); 1829 let rkey = rkey(); ··· 1867 None 1868 }; 1869 1870 + // Render HTML 1871 + let rendered_html = { 1872 + let author_did = match &ident { 1873 + AtIdentifier::Did(d) => d.clone().into_static(), 1874 + AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1875 + }; 1876 + let ctx = PcktRenderContext::new(author_did); 1877 + if let Some(blocks) = &doc.content { 1878 + Some(render_content_blocks(blocks, &ctx, &fetcher).await) 1879 + } else { 1880 + Some(String::from("<p>No content</p>")) 1881 + } 1882 + }; 1883 + 1884 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1885 1886 Some(( 1887 serde_json::to_value(&doc).ok()?, 1888 serde_json::to_value(&*profile).ok()?, 1889 publication_url, 1890 + rendered_html, 1891 )) 1892 } 1893 }); ··· 1897 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1898 1899 let res = res.as_ref().ok()?; 1900 + if let Some(Some((doc_json, profile_json, publication_url, rendered_html))) = &*res.read() { 1901 let document = 1902 jacquard::from_json_value::<SiteStandardDocument>(doc_json.clone()).ok()?; 1903 let profile = ··· 1906 document, 1907 profile, 1908 publication_url: publication_url.clone(), 1909 + rendered_html: rendered_html.clone(), 1910 }) 1911 } else { 1912 None ··· 1925 Memo<Option<crate::fetch::PcktDocumentData>>, 1926 ) { 1927 use jacquard::IntoStatic; 1928 + use jacquard::prelude::IdentityResolver; 1929 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1930 use weaver_api::site_standard::publication::Publication; 1931 + use weaver_renderer::pckt::{PcktRenderContext, render_content_blocks}; 1932 1933 let fetcher = use_context::<crate::fetch::Fetcher>(); 1934 ··· 1977 None 1978 }; 1979 1980 + // Render HTML 1981 + let rendered_html = { 1982 + let author_did = match &ident { 1983 + AtIdentifier::Did(d) => d.clone().into_static(), 1984 + AtIdentifier::Handle(h) => fetcher.resolve_handle(h).await.ok()?.into_static(), 1985 + }; 1986 + let ctx = PcktRenderContext::new(author_did); 1987 + if let Some(blocks) = &doc.content { 1988 + Some(render_content_blocks(blocks, &ctx, &fetcher).await) 1989 + } else { 1990 + Some(String::from("<p>No content</p>")) 1991 + } 1992 + }; 1993 + 1994 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1995 1996 Some(crate::fetch::PcktDocumentData { 1997 document: doc, 1998 profile: (*profile).clone(), 1999 publication_url, 2000 + rendered_html, 2001 }) 2002 } 2003 });
+4
crates/weaver-app/src/fetch.rs
··· 90 pub profile: ProfileDataView<'static>, 91 /// Publication base_path for constructing external URL (e.g., "connectedplaces.leaflet.pub") 92 pub publication_base_path: Option<String>, 93 } 94 95 /// Data for a site.standard / blog.pckt document ··· 100 pub profile: ProfileDataView<'static>, 101 /// Publication URL for constructing external URL (e.g., "https://crypto.pckt.blog") 102 pub publication_url: Option<String>, 103 } 104 105 pub struct Client {
··· 90 pub profile: ProfileDataView<'static>, 91 /// Publication base_path for constructing external URL (e.g., "connectedplaces.leaflet.pub") 92 pub publication_base_path: Option<String>, 93 + /// Pre-rendered HTML content 94 + pub rendered_html: Option<String>, 95 } 96 97 /// Data for a site.standard / blog.pckt document ··· 102 pub profile: ProfileDataView<'static>, 103 /// Publication URL for constructing external URL (e.g., "https://crypto.pckt.blog") 104 pub publication_url: Option<String>, 105 + /// Pre-rendered HTML content 106 + pub rendered_html: Option<String>, 107 } 108 109 pub struct Client {
+3 -3
crates/weaver-app/src/views/entry.rs
··· 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } 78 NotebookCss { ident: ident().to_smolstr(), notebook: book_title.clone() } 79 80 - div { class: "entry-page-layout", 81 if let Some(ref prev) = book_entry_view.prev { 82 div { class: "nav-gutter nav-prev", 83 NavButton { ··· 133 DefaultNotebookCss {} 134 135 136 - div { class: "entry-page-layout", 137 div { class: "entry-content-main notebook-content", 138 { 139 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); ··· 234 document::Link { rel: "stylesheet", href: ENTRY_CSS } 235 NotebookCss { ident: ident().to_smolstr(), notebook: book_title() } 236 237 - div { class: "entry-page-layout", 238 if let Some(ref prev) = book_entry_view.prev { 239 div { class: "nav-gutter nav-prev", 240 NavButton {
··· 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } 78 NotebookCss { ident: ident().to_smolstr(), notebook: book_title.clone() } 79 80 + div { class: "entry-page", 81 if let Some(ref prev) = book_entry_view.prev { 82 div { class: "nav-gutter nav-prev", 83 NavButton { ··· 133 DefaultNotebookCss {} 134 135 136 + div { class: "entry-page", 137 div { class: "entry-content-main notebook-content", 138 { 139 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); ··· 234 document::Link { rel: "stylesheet", href: ENTRY_CSS } 235 NotebookCss { ident: ident().to_smolstr(), notebook: book_title() } 236 237 + div { class: "entry-page", 238 if let Some(ref prev) = book_entry_view.prev { 239 div { class: "nav-gutter nav-prev", 240 NavButton {
+17 -127
crates/weaver-app/src/views/external.rs
··· 7 8 use crate::components::css::DefaultNotebookCss; 9 use crate::components::{AuthorList, extract_author_info}; 10 - use crate::fetch::Fetcher; 11 12 #[component] 13 pub fn WhiteWindEntry( ··· 71 document::Link { rel: "stylesheet", href: ENTRY_CSS } 72 DefaultNotebookCss {} 73 74 - div { class: "entry-page-layout", 75 div { class: "entry-content-main notebook-content", 76 header { class: "entry-metadata", 77 div { class: "entry-header-row", ··· 141 rkey: ReadSignal<SmolStr>, 142 ) -> Element { 143 use crate::components::{ENTRY_CSS, EntryOgMeta}; 144 - use weaver_api::pub_leaflet::document::DocumentPagesItem; 145 146 let (entry_res, entry_data) = crate::data::use_leaflet_document_data(ident, rkey); 147 ··· 170 .record(data.profile.clone()) 171 .build(); 172 173 - let pages = data.document.pages.clone(); 174 - let author_did = data.document.author.clone(); 175 - 176 rsx! { 177 EntryOgMeta { 178 title: title.to_string(), ··· 184 document::Link { rel: "stylesheet", href: ENTRY_CSS } 185 DefaultNotebookCss {} 186 187 - div { class: "entry-page-layout", 188 div { class: "entry-content-main notebook-content", 189 header { class: "entry-metadata", 190 div { class: "entry-header-row", ··· 206 } 207 } 208 } 209 - LeafletContent { 210 - pages: pages, 211 - author_did: author_did, 212 } 213 } 214 } ··· 218 } 219 } 220 221 - #[component] 222 - fn LeafletContent( 223 - pages: Vec<weaver_api::pub_leaflet::document::DocumentPagesItem<'static>>, 224 - author_did: jacquard::types::string::AtIdentifier<'static>, 225 - ) -> Element { 226 - use jacquard::IntoStatic; 227 - use jacquard::prelude::*; 228 - use weaver_api::pub_leaflet::document::DocumentPagesItem; 229 - use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 230 - 231 - let fetcher = use_context::<Fetcher>(); 232 - 233 - let html = use_resource(move || { 234 - let pages = pages.clone(); 235 - let author_did = author_did.clone(); 236 - let fetcher = fetcher.clone(); 237 - async move { 238 - let mut html = String::new(); 239 - 240 - // Resolve author DID 241 - let did = match &author_did { 242 - jacquard::types::string::AtIdentifier::Did(d) => d.clone().into_static(), 243 - jacquard::types::string::AtIdentifier::Handle(h) => { 244 - match fetcher.resolve_handle(h).await { 245 - Ok(d) => d.into_static(), 246 - Err(_) => return String::from("<p>Failed to resolve author</p>"), 247 - } 248 - } 249 - }; 250 - 251 - let ctx = LeafletRenderContext::new(did); 252 - 253 - for page in &pages { 254 - match page { 255 - DocumentPagesItem::LinearDocument(linear_doc) => { 256 - html.push_str(&render_linear_document(linear_doc, &ctx, &fetcher).await); 257 - } 258 - DocumentPagesItem::Canvas(_) => { 259 - html.push_str("<div class=\"embed-video-placeholder\">[Canvas layout not yet supported]</div>"); 260 - } 261 - DocumentPagesItem::Unknown(_) => { 262 - html.push_str( 263 - "<div class=\"embed-video-placeholder\">[Unknown page type]</div>", 264 - ); 265 - } 266 - } 267 - } 268 - 269 - html 270 - } 271 - }); 272 - 273 - match &*html.read() { 274 - Some(content) => rsx! { 275 - div { 276 - class: "entry leaflet-document", 277 - dangerous_inner_html: "{content}" 278 - } 279 - }, 280 - None => rsx! { p { "Rendering..." } }, 281 - } 282 - } 283 - 284 #[cfg(feature = "pckt")] 285 #[component] 286 pub fn PcktEntry(ident: ReadSignal<AtIdentifier<'static>>, rkey: ReadSignal<SmolStr>) -> Element { ··· 327 .format("%B %d, %Y") 328 .to_string(); 329 330 - let content = data.document.content.clone(); 331 - let author_did = ident(); 332 - 333 // Build external URL from publication URL + path (or rkey) 334 let doc_path = data 335 .document ··· 349 document::Link { rel: "stylesheet", href: ENTRY_CSS } 350 DefaultNotebookCss {} 351 352 - div { class: "entry-page-layout", 353 div { class: "entry-content-main notebook-content", 354 header { class: "entry-metadata", 355 div { class: "entry-header-row", ··· 379 } 380 } 381 } 382 - PcktContent { 383 - content: content, 384 - author_did: author_did, 385 } 386 } 387 } 388 } 389 } 390 None => rsx! { p { "Loading..." } }, 391 - } 392 - } 393 - 394 - #[cfg(feature = "pckt")] 395 - #[component] 396 - fn PcktContent( 397 - content: Option<Vec<jacquard::types::value::Data<'static>>>, 398 - author_did: AtIdentifier<'static>, 399 - ) -> Element { 400 - use jacquard::IntoStatic; 401 - use jacquard::prelude::*; 402 - use weaver_renderer::pckt::{PcktRenderContext, render_content_blocks}; 403 - 404 - let fetcher = use_context::<Fetcher>(); 405 - 406 - let html = use_resource(move || { 407 - let content = content.clone(); 408 - let author_did = author_did.clone(); 409 - let fetcher = fetcher.clone(); 410 - async move { 411 - // Resolve author DID 412 - let did = match &author_did { 413 - AtIdentifier::Did(d) => d.clone().into_static(), 414 - AtIdentifier::Handle(h) => match fetcher.resolve_handle(h).await { 415 - Ok(d) => d.into_static(), 416 - Err(_) => return String::from("<p>Failed to resolve author</p>"), 417 - }, 418 - }; 419 - 420 - let ctx = PcktRenderContext::new(did); 421 - 422 - if let Some(blocks) = &content { 423 - render_content_blocks(blocks, &ctx, &fetcher).await 424 - } else { 425 - String::from("<p>No content</p>") 426 - } 427 - } 428 - }); 429 - 430 - match &*html.read() { 431 - Some(content) => rsx! { 432 - div { 433 - class: "entry pckt-document", 434 - dangerous_inner_html: "{content}" 435 - } 436 - }, 437 - None => rsx! { p { "Rendering..." } }, 438 } 439 } 440
··· 7 8 use crate::components::css::DefaultNotebookCss; 9 use crate::components::{AuthorList, extract_author_info}; 10 11 #[component] 12 pub fn WhiteWindEntry( ··· 70 document::Link { rel: "stylesheet", href: ENTRY_CSS } 71 DefaultNotebookCss {} 72 73 + div { class: "entry-page", 74 div { class: "entry-content-main notebook-content", 75 header { class: "entry-metadata", 76 div { class: "entry-header-row", ··· 140 rkey: ReadSignal<SmolStr>, 141 ) -> Element { 142 use crate::components::{ENTRY_CSS, EntryOgMeta}; 143 144 let (entry_res, entry_data) = crate::data::use_leaflet_document_data(ident, rkey); 145 ··· 168 .record(data.profile.clone()) 169 .build(); 170 171 rsx! { 172 EntryOgMeta { 173 title: title.to_string(), ··· 179 document::Link { rel: "stylesheet", href: ENTRY_CSS } 180 DefaultNotebookCss {} 181 182 + div { class: "entry-page", 183 div { class: "entry-content-main notebook-content", 184 header { class: "entry-metadata", 185 div { class: "entry-header-row", ··· 201 } 202 } 203 } 204 + if let Some(ref html) = data.rendered_html { 205 + div { 206 + class: "entry leaflet-document", 207 + dangerous_inner_html: "{html}" 208 + } 209 + } else { 210 + p { "Rendering..." } 211 } 212 } 213 } ··· 217 } 218 } 219 220 #[cfg(feature = "pckt")] 221 #[component] 222 pub fn PcktEntry(ident: ReadSignal<AtIdentifier<'static>>, rkey: ReadSignal<SmolStr>) -> Element { ··· 263 .format("%B %d, %Y") 264 .to_string(); 265 266 // Build external URL from publication URL + path (or rkey) 267 let doc_path = data 268 .document ··· 282 document::Link { rel: "stylesheet", href: ENTRY_CSS } 283 DefaultNotebookCss {} 284 285 + div { class: "entry-page", 286 div { class: "entry-content-main notebook-content", 287 header { class: "entry-metadata", 288 div { class: "entry-header-row", ··· 312 } 313 } 314 } 315 + if let Some(ref html) = data.rendered_html { 316 + div { 317 + class: "entry pckt-document", 318 + dangerous_inner_html: "{html}" 319 + } 320 + } else { 321 + p { "Rendering..." } 322 } 323 } 324 } 325 } 326 } 327 None => rsx! { p { "Loading..." } }, 328 } 329 } 330
+1 -2
crates/weaver-renderer/src/css.rs
··· 132 /* When sidenotes exist, body padding creates the gutter */ 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 body:has(.sidenote) {{ 135 - padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 padding-inline-end: 15.5rem; 137 }} 138 ··· 260 font-size: 0.95em; 261 border-bottom-right-radius: 5px; 262 border-top-right-radius: 5px; 263 - }} 264 }} 265 266 /* Tables */
··· 132 /* When sidenotes exist, body padding creates the gutter */ 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 body:has(.sidenote) {{ 135 + padding-inline-start: clamp(1rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 padding-inline-end: 15.5rem; 137 }} 138 ··· 260 font-size: 0.95em; 261 border-bottom-right-radius: 5px; 262 border-top-right-radius: 5px; 263 }} 264 265 /* Tables */
+95 -61
test-sidenotes.html
··· 84 /* When sidenotes exist, body padding creates the gutter */ 85 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 86 body:has(.sidenote) { 87 - padding-left: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 88 - padding-right: 15.5rem; 89 } 90 91 /* Typography */ ··· 154 155 /* Lists */ 156 ul, ol { 157 - margin-left: 1rem; 158 margin-bottom: 1rem; 159 } 160 ··· 202 203 /* Blockquotes */ 204 blockquote { 205 - border-left: 2px solid var(--color-secondary); 206 background: var(--color-surface); 207 - padding-left: 1rem; 208 - padding-right: 1rem; 209 padding-top: 0.5rem; 210 padding-bottom: 0.04rem; 211 margin: 1rem 0; ··· 228 th, td { 229 border: 1px solid var(--color-border); 230 padding: 0.5rem; 231 - text-align: left; 232 } 233 234 th { ··· 270 271 .footnote-definition-label { 272 font-weight: 600; 273 - margin-right: 0.5rem; 274 color: var(--color-primary); 275 } 276 277 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 278 .notebook-content aside, 279 .notebook-content .aside { 280 - float: left; 281 width: 40%; 282 margin: 0 1.5rem 1rem 0; 283 padding: 1rem; 284 background: var(--color-surface); 285 - border-right: 3px solid var(--color-primary); 286 font-size: 0.9em; 287 - clear: left; 288 } 289 290 .notebook-content aside > *:first-child, ··· 300 /* Reset blockquote styling inside asides */ 301 .notebook-content aside > blockquote, 302 .notebook-content .aside > blockquote { 303 - border-left: none; 304 background: transparent; 305 padding: 0; 306 margin: 0; ··· 308 } 309 310 /* Indent utilities */ 311 - .indent-1 { margin-left: 1em; } 312 - .indent-2 { margin-left: 2em; } 313 - .indent-3 { margin-left: 3em; } 314 315 /* Tufte-style Sidenotes */ 316 /* Hide checkbox for sidenote toggle */ ··· 329 position: relative; 330 top: -0.5em; 331 color: var(--color-primary); 332 - padding-left: 0.1em; 333 } 334 335 /* Sidenote content (margin notes on wide screens) */ 336 .sidenote { 337 - float: right; 338 - clear: right; 339 - margin-right: -15.5rem; 340 width: 14rem; 341 margin-top: 0.3rem; 342 margin-bottom: 1rem; ··· 354 @media (max-width: 900px) { 355 /* Reset sidenote gutter on mobile */ 356 body:has(.sidenote) { 357 - padding-right: 0; 358 } 359 360 aside, .aside { ··· 374 margin: 0.5rem 2.5%; 375 padding: 0.5rem; 376 background: var(--color-surface); 377 - border-left: 2px solid var(--color-primary); 378 } 379 380 label.sidenote-number { ··· 412 margin: 1rem 0; 413 padding: 1rem; 414 background: var(--color-surface); 415 - border-left: 2px solid var(--color-secondary); 416 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 417 } 418 419 .atproto-embed:hover { 420 - border-left-color: var(--color-primary); 421 } 422 423 @media (prefers-color-scheme: dark) { 424 .atproto-embed { 425 box-shadow: none; 426 border: 1px solid var(--color-border); 427 - border-left: 2px solid var(--color-secondary); 428 } 429 } 430 ··· 601 } 602 603 .embed-external:hover { 604 - border-left: 2px solid var(--color-primary); 605 - margin-left: -1px; 606 } 607 608 @media (prefers-color-scheme: dark) { ··· 611 } 612 613 .embed-external:hover { 614 - border-left: 2px solid var(--color-primary); 615 - margin-left: -1px; 616 } 617 } 618 ··· 698 margin-top: 0.5rem; 699 padding: 0.75rem; 700 background: var(--color-overlay); 701 - border-left: 2px solid var(--color-tertiary); 702 } 703 704 @media (prefers-color-scheme: dark) { 705 .embed-quote { 706 border: 1px solid var(--color-border); 707 - border-left: 2px solid var(--color-tertiary); 708 } 709 } 710 ··· 735 display: block; 736 padding: 1rem; 737 background: var(--color-overlay); 738 - border-left: 2px solid var(--color-border); 739 color: var(--color-muted); 740 font-style: italic; 741 margin-top: 0.5rem; ··· 759 margin-top: 0.5rem; 760 padding: 0.75rem; 761 background: var(--color-overlay); 762 - border-left: 2px solid var(--color-tertiary); 763 } 764 765 .embed-record-card > .embed-author-name { ··· 802 .embed-fields .embed-fields { 803 display: block; 804 margin-top: 0.5rem; 805 - margin-left: 1rem; 806 - padding-left: 0.5rem; 807 - border-left: 1px solid var(--color-border); 808 } 809 810 /* Type label inside fields should be block with spacing */ ··· 919 padding: 0; 920 background: var(--color-surface); 921 border: 1px solid var(--color-border); 922 - border-left: 1px solid var(--color-border); 923 box-shadow: none; 924 overflow: hidden; 925 } 926 927 .atproto-entry:hover { 928 - border-left-color: var(--color-border); 929 } 930 931 @media (prefers-color-scheme: dark) { 932 .atproto-entry { 933 border: 1px solid var(--color-border); 934 - border-left: 1px solid var(--color-border); 935 } 936 } 937 ··· 1027 h3 { font-size: 1.2rem; } 1028 1029 blockquote { 1030 - margin-left: 0; 1031 - margin-right: 0; 1032 } 1033 } 1034 ··· 1043 h3 { font-size: 1.1rem; } 1044 1045 blockquote { 1046 - padding-left: 0.75rem; 1047 - padding-right: 0.75rem; 1048 } 1049 } 1050 </style> 1051 <style> 1052 /* Syntax highlighting - Light Mode (default) */ ··· 1236 <body style="background: var(--color-base); min-height: 100vh;"> 1237 <div class="notebook-content"> 1238 <h1>Weaver: Long-form Writing on AT Protocol</h1> 1239 - <p><em>Or: "Get in kid, we're rebuilding the blogosphere!"</em></p> 1240 - <p>I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal.<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Hi Rahaeli. Sorry I was the wrong kind of nerd.</span></p> 1241 <blockquote> 1242 - <p><img src="weaver_photo_med.jpg" alt="weaver_photo_med.jpg" /><em>The namesake of what I'm building</em></p> 1243 </blockquote> 1244 - <p>Social media in the conventional sense has been in a lot of ways a small part of the story of my time on the internet. The broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there.</p> 1245 <h2>The Blogosphere</h2> 1246 - <p>I am an atheist in large part because of a blog called Common Sense Atheism.<label for="sn-2" class="sidenote-number"></label><input type="checkbox" id="sn-2" class="margin-toggle"/><span class="sidenote">The author, Luke Muehlhauser, was criticising both Richard Dawkins <em>and</em> some Christian apologetics I was familiar with.</span> Luke's blog was part of a cluster of blogs out of which grew the rationalists, one of, for better or for worse, the most influential intellectual movements of the 21st century.I also read blogs like boingboing.net, was a big fan of Cory Doctorow. I figured out I am trans in part because of Thing of Things,<label for="sn-3" class="sidenote-number"></label><input type="checkbox" id="sn-3" class="margin-toggle"/><span class="sidenote">Specifically their piece on the <a href="https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/">cluster structure of genderspace</a>.</span> a blog by Ozy Frantz, a transmasc person in the broader rationalist and Effective Altruist blogosphere.One thing these all have in common is length. Part of the reason I only really got onto Twitter in 2020 or so was because the concept of microblogging, of having to fit your thoughts into such a small package, baffled me for ages.<label for="sn-4" class="sidenote-number"></label><input type="checkbox" id="sn-4" class="margin-toggle"/><span class="sidenote">Amusingly I now think that being on Twitter and now Bluesky made me a better writer. Restrictions breed creativity, after all.</span></p> 1247 <aside> 1248 <blockquote> 1249 - <p><strong>On Platform Decay</strong></p> 1250 - <p>Through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress required too much setup. Tumblr's system for comments remains insane. Hosting my own seemed like too much money to burn on something nobody might read.</p> 1251 </blockquote> 1252 </aside> 1253 - <p>But at the same time, Substack's success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts.</p> 1254 - <p>Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists.<label for="sn-5" class="sidenote-number"></label><input type="checkbox" id="sn-5" class="margin-toggle"/><span class="sidenote">I am very much a fan of freedom of expression. I'm not so much a fan of paying money to Nazis.</span>That's where the <code>at://</code> protocol and Weaver comes in.</p> 1255 <h2>The Pitch</h2> 1256 - <p>Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto.<label for="sn-6" class="sidenote-number"></label><input type="checkbox" id="sn-6" class="margin-toggle"/><span class="sidenote">The weaver bird builds intricate, self-contained homes—seemed fitting for a platform about owning your writing.</span> I was inspired by how weaver birds build their own homes, and by the notebooks, physical and virtual, that I create in the course of my work.The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files into a static "notebook" site. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app.</p> 1257 <aside> 1258 <blockquote> 1259 - <p><strong>The Ultimate Goal</strong></p> 1260 - <p>Build a platform suitable for professional writers and journalists, an open alternative to platforms like Substack, with ways for readers to support writers, all on the <code>at://</code> protocol.</p> 1261 </blockquote> 1262 </aside> 1263 <h2>How It Works</h2> 1264 - <p>Weaver works on a concept of notebooks with entries, which can be grouped into pages or chapters. They can have multiple attributed authors. You can tear out a metaphorical page and stick it in another notebook.</p> 1265 - <p>You own what you write.<label for="sn-7" class="sidenote-number"></label><input type="checkbox" id="sn-7" class="margin-toggle"/><span class="sidenote">Technically you can include entries you don't control in your notebooks, although this isn't a supported mode—it's about <em>your</em> ownership of <em>your</em> words.</span> And once collaborative editing is in, collaborative work will be resilient against deletion by one author. They can delete their notebook or even their account, but what you write will be safe.Entries are Markdown text—specifically, an extension on the Obsidian flavour of Markdown.<label for="sn-8" class="sidenote-number"></label><input type="checkbox" id="sn-8" class="margin-toggle"/><span class="sidenote">I forked the popular rust markdown processing library <code>pulldown-cmark</code> because it had limited extensibility along the axes I wanted—custom syntax extensions to support Obsidian's Markdown flavour and additional useful features, like some of the ones on show here!</span> They support additional embed types, including atproto record embeds and other markdown documents, as well as resizable images.</p> 1266 <h2>Why Rust?</h2> 1267 - <p>As to why I'm writing it in Rust (and currently zero Typescript) as opposed to Go and Typescript? Well it comes down to familiarity. Rust isn't necessarily anyone's first choice in a vacuum for a web-native programming language, but it works quite well as one. I can share the vast majority of the protocol code, as well as the markdown rendering engine, between front and back end, with few if any compromises on performance, save a larger bundle size due to the nature of WebAssembly.</p> 1268 <aside> 1269 <blockquote> 1270 - <p><strong>On Interoperability</strong></p> 1271 - <p>The <code>at://</code> protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macroblogging" too. Weaver's app server can display Whitewind posts. With effort, it can faithfully render Leaflet posts. It doesn't care what app your profile is on.</p> 1272 </blockquote> 1273 </aside> 1274 <h2>Evolution</h2> 1275 - <p>Weaver is therefore very much an evolving thing. It will always have and support the proof-of-concept workflow as a first-class citizen. That's part of the benefit of building this on atproto.</p> 1276 - <p>If I screw this up, not too hard for someone else to pick up the torch and continue.<label for="sn-9" class="sidenote-number"></label><input type="checkbox" id="sn-9" class="margin-toggle"/><span class="sidenote">This is the traditional footnote, at the end, because sometimes you want your citations at the bottom of the page rather than in the margins.</span></p> 1277 </div> 1278 </body> 1279 </html>
··· 84 /* When sidenotes exist, body padding creates the gutter */ 85 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 86 body:has(.sidenote) { 87 + padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 88 + padding-inline-end: 15.5rem; 89 } 90 91 /* Typography */ ··· 154 155 /* Lists */ 156 ul, ol { 157 + margin-inline-start: 1rem; 158 margin-bottom: 1rem; 159 } 160 ··· 202 203 /* Blockquotes */ 204 blockquote { 205 + border-inline-start: 2px solid var(--color-secondary); 206 background: var(--color-surface); 207 + padding-inline-start: 1rem; 208 + padding-inline-end: 1rem; 209 padding-top: 0.5rem; 210 padding-bottom: 0.04rem; 211 margin: 1rem 0; ··· 228 th, td { 229 border: 1px solid var(--color-border); 230 padding: 0.5rem; 231 + text-align: start; 232 } 233 234 th { ··· 270 271 .footnote-definition-label { 272 font-weight: 600; 273 + margin-inline-end: 0.5rem; 274 color: var(--color-primary); 275 } 276 277 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 278 .notebook-content aside, 279 .notebook-content .aside { 280 + float: inline-start; 281 width: 40%; 282 margin: 0 1.5rem 1rem 0; 283 padding: 1rem; 284 background: var(--color-surface); 285 + border-inline-end: 3px solid var(--color-primary); 286 font-size: 0.9em; 287 + clear: inline-start; 288 } 289 290 .notebook-content aside > *:first-child, ··· 300 /* Reset blockquote styling inside asides */ 301 .notebook-content aside > blockquote, 302 .notebook-content .aside > blockquote { 303 + border-inline-start: none; 304 background: transparent; 305 padding: 0; 306 margin: 0; ··· 308 } 309 310 /* Indent utilities */ 311 + .indent-1 { margin-inline-start: 1em; } 312 + .indent-2 { margin-inline-start: 2em; } 313 + .indent-3 { margin-inline-start: 3em; } 314 315 /* Tufte-style Sidenotes */ 316 /* Hide checkbox for sidenote toggle */ ··· 329 position: relative; 330 top: -0.5em; 331 color: var(--color-primary); 332 + padding-inline-start: 0.1em; 333 } 334 335 /* Sidenote content (margin notes on wide screens) */ 336 .sidenote { 337 + float: inline-end; 338 + clear: inline-end; 339 + margin-inline-end: -15.5rem; 340 width: 14rem; 341 margin-top: 0.3rem; 342 margin-bottom: 1rem; ··· 354 @media (max-width: 900px) { 355 /* Reset sidenote gutter on mobile */ 356 body:has(.sidenote) { 357 + padding-inline-end: 0; 358 } 359 360 aside, .aside { ··· 374 margin: 0.5rem 2.5%; 375 padding: 0.5rem; 376 background: var(--color-surface); 377 + border-inline-start: 2px solid var(--color-primary); 378 } 379 380 label.sidenote-number { ··· 412 margin: 1rem 0; 413 padding: 1rem; 414 background: var(--color-surface); 415 + border-inline-start: 2px solid var(--color-secondary); 416 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 417 } 418 419 .atproto-embed:hover { 420 + border-inline-start-color: var(--color-primary); 421 } 422 423 @media (prefers-color-scheme: dark) { 424 .atproto-embed { 425 box-shadow: none; 426 border: 1px solid var(--color-border); 427 + border-inline-start: 2px solid var(--color-secondary); 428 } 429 } 430 ··· 601 } 602 603 .embed-external:hover { 604 + border-inline-start: 2px solid var(--color-primary); 605 + margin-inline-start: -1px; 606 } 607 608 @media (prefers-color-scheme: dark) { ··· 611 } 612 613 .embed-external:hover { 614 + border-inline-start: 2px solid var(--color-primary); 615 + margin-inline-start: -1px; 616 } 617 } 618 ··· 698 margin-top: 0.5rem; 699 padding: 0.75rem; 700 background: var(--color-overlay); 701 + border-inline-start: 2px solid var(--color-tertiary); 702 } 703 704 @media (prefers-color-scheme: dark) { 705 .embed-quote { 706 border: 1px solid var(--color-border); 707 + border-inline-start: 2px solid var(--color-tertiary); 708 } 709 } 710 ··· 735 display: block; 736 padding: 1rem; 737 background: var(--color-overlay); 738 + border-inline-start: 2px solid var(--color-border); 739 color: var(--color-muted); 740 font-style: italic; 741 margin-top: 0.5rem; ··· 759 margin-top: 0.5rem; 760 padding: 0.75rem; 761 background: var(--color-overlay); 762 + border-inline-start: 2px solid var(--color-tertiary); 763 } 764 765 .embed-record-card > .embed-author-name { ··· 802 .embed-fields .embed-fields { 803 display: block; 804 margin-top: 0.5rem; 805 + margin-inline-start: 1rem; 806 + padding-inline-start: 0.5rem; 807 + border-inline-start: 1px solid var(--color-border); 808 } 809 810 /* Type label inside fields should be block with spacing */ ··· 919 padding: 0; 920 background: var(--color-surface); 921 border: 1px solid var(--color-border); 922 + border-inline-start: 1px solid var(--color-border); 923 box-shadow: none; 924 overflow: hidden; 925 } 926 927 .atproto-entry:hover { 928 + border-inline-start-color: var(--color-border); 929 } 930 931 @media (prefers-color-scheme: dark) { 932 .atproto-entry { 933 border: 1px solid var(--color-border); 934 + border-inline-start: 1px solid var(--color-border); 935 } 936 } 937 ··· 1027 h3 { font-size: 1.2rem; } 1028 1029 blockquote { 1030 + margin-inline-start: 0; 1031 + margin-inline-end: 0; 1032 } 1033 } 1034 ··· 1043 h3 { font-size: 1.1rem; } 1044 1045 blockquote { 1046 + padding-inline-start: 0.75rem; 1047 + padding-inline-end: 0.75rem; 1048 } 1049 } 1050 + 1051 + /* Leaflet document embeds */ 1052 + .atproto-leaflet { 1053 + max-width: none; 1054 + width: 100%; 1055 + margin: 1rem 0; 1056 + } 1057 + 1058 + .leaflet-document { 1059 + display: block; 1060 + } 1061 + 1062 + .leaflet-text { 1063 + margin: 0.5rem 0; 1064 + } 1065 + 1066 + .leaflet-button { 1067 + display: inline-block; 1068 + padding: 0.5rem 1rem; 1069 + background: var(--color-primary); 1070 + color: var(--color-base); 1071 + text-decoration: none; 1072 + border-radius: 4px; 1073 + margin: 0.5rem 0; 1074 + } 1075 + 1076 + .leaflet-button:hover { 1077 + opacity: 0.9; 1078 + } 1079 + 1080 + /* Alignment utilities */ 1081 + .align-center { text-align: center; } 1082 + .align-right { text-align: right; } 1083 + .align-justify { text-align: justify; } 1084 </style> 1085 <style> 1086 /* Syntax highlighting - Light Mode (default) */ ··· 1270 <body style="background: var(--color-base); min-height: 100vh;"> 1271 <div class="notebook-content"> 1272 <h1>Weaver: Long-form Writing on AT Protocol</h1> 1273 + <em><p dir="ltr">Or: "Get in kid, we're rebuilding the blogosphere!"</em></p> 1274 + <p dir="ltr">I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal.<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Hi Rahaeli. Sorry I was the wrong kind of nerd.</span></p> 1275 <blockquote> 1276 + <img src="weaver_photo_med.jpg" alt="weaver_photo_med.jpg" /><em><p dir="ltr">The namesake of what I'm building</em></p> 1277 </blockquote> 1278 + <p dir="ltr">Social media in the conventional sense has been in a lot of ways a small part of the story of my time on the internet. The broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there.</p> 1279 <h2>The Blogosphere</h2> 1280 + <p dir="ltr">I am an atheist in large part because of a blog called Common Sense Atheism.<label for="sn-2" class="sidenote-number"></label><input type="checkbox" id="sn-2" class="margin-toggle"/><span class="sidenote">The author, Luke Muehlhauser, was criticising both Richard Dawkins <em>and</em> some Christian apologetics I was familiar with.</span> Luke's blog was part of a cluster of blogs out of which grew the rationalists, one of, for better or for worse, the most influential intellectual movements of the 21st century.I also read blogs like boingboing.net, was a big fan of Cory Doctorow. I figured out I am trans in part because of Thing of Things,<label for="sn-3" class="sidenote-number"></label><input type="checkbox" id="sn-3" class="margin-toggle"/><span class="sidenote">Specifically their piece on the <a href="https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/">cluster structure of genderspace</a>.</span> a blog by Ozy Frantz, a transmasc person in the broader rationalist and Effective Altruist blogosphere.One thing these all have in common is length. Part of the reason I only really got onto Twitter in 2020 or so was because the concept of microblogging, of having to fit your thoughts into such a small package, baffled me for ages.<label for="sn-4" class="sidenote-number"></label><input type="checkbox" id="sn-4" class="margin-toggle"/><span class="sidenote">Amusingly I now think that being on Twitter and now Bluesky made me a better writer. Restrictions breed creativity, after all.</span></p> 1281 <aside> 1282 <blockquote> 1283 + <strong><p dir="ltr">On Platform Decay</strong> 1284 + Through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress required too much setup. Tumblr's system for comments remains insane. Hosting my own seemed like too much money to burn on something nobody might read.</p> 1285 </blockquote> 1286 </aside> 1287 + <p dir="ltr">But at the same time, Substack's success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts.</p> 1288 + <p dir="ltr">Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists.<label for="sn-5" class="sidenote-number"></label><input type="checkbox" id="sn-5" class="margin-toggle"/><span class="sidenote">I am very much a fan of freedom of expression. I'm not so much a fan of paying money to Nazis.</span>That's where the <code>at://</code> protocol and Weaver comes in.</p> 1289 <h2>The Pitch</h2> 1290 + <p dir="ltr">Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto.<label for="sn-6" class="sidenote-number"></label><input type="checkbox" id="sn-6" class="margin-toggle"/><span class="sidenote">The weaver bird builds intricate, self-contained homes—seemed fitting for a platform about owning your writing.</span> I was inspired by how weaver birds build their own homes, and by the notebooks, physical and virtual, that I create in the course of my work.The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files into a static "notebook" site. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app.</p> 1291 <aside> 1292 <blockquote> 1293 + <strong><p dir="ltr">The Ultimate Goal</strong></p> 1294 + <p dir="ltr">Build a platform suitable for professional writers and journalists, an open alternative to platforms like Substack, with ways for readers to support writers, all on the <code>at://</code> protocol.</p> 1295 </blockquote> 1296 </aside> 1297 <h2>How It Works</h2> 1298 + <p dir="ltr">Weaver works on a concept of notebooks with entries, which can be grouped into pages or chapters. They can have multiple attributed authors. You can tear out a metaphorical page and stick it in another notebook.</p> 1299 + <p dir="ltr">You own what you write.<label for="sn-7" class="sidenote-number"></label><input type="checkbox" id="sn-7" class="margin-toggle"/><span class="sidenote">Technically you can include entries you don't control in your notebooks, although this isn't a supported mode—it's about <em>your</em> ownership of <em>your</em> words.</span> And once collaborative editing is in, collaborative work will be resilient against deletion by one author. They can delete their notebook or even their account, but what you write will be safe.Entries are Markdown text—specifically, an extension on the Obsidian flavour of Markdown.<label for="sn-8" class="sidenote-number"></label><input type="checkbox" id="sn-8" class="margin-toggle"/><span class="sidenote">I forked the popular rust markdown processing library <code>pulldown-cmark</code> because it had limited extensibility along the axes I wanted—custom syntax extensions to support Obsidian's Markdown flavour and additional useful features, like some of the ones on show here!</span> They support additional embed types, including atproto record embeds and other markdown documents, as well as resizable images.</p> 1300 <h2>Why Rust?</h2> 1301 + <p dir="ltr">As to why I'm writing it in Rust (and currently zero Typescript) as opposed to Go and Typescript? Well it comes down to familiarity. Rust isn't necessarily anyone's first choice in a vacuum for a web-native programming language, but it works quite well as one. I can share the vast majority of the protocol code, as well as the markdown rendering engine, between front and back end, with few if any compromises on performance, save a larger bundle size due to the nature of WebAssembly.</p> 1302 <aside> 1303 <blockquote> 1304 + <strong><p dir="ltr">On Interoperability</strong></p> 1305 + <p dir="ltr">The <code>at://</code> protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macroblogging" too. Weaver's app server can display Whitewind posts. With effort, it can faithfully render Leaflet posts. It doesn't care what app your profile is on.</p> 1306 </blockquote> 1307 </aside> 1308 <h2>Evolution</h2> 1309 + <p dir="ltr">Weaver is therefore very much an evolving thing. It will always have and support the proof-of-concept workflow as a first-class citizen. That's part of the benefit of building this on atproto.</p> 1310 + <p dir="ltr">If I screw this up, not too hard for someone else to pick up the torch and continue.<label for="sn-9" class="sidenote-number"></label><input type="checkbox" id="sn-9" class="margin-toggle"/><span class="sidenote">This is the traditional footnote, at the end, because sometimes you want your citations at the bottom of the page rather than in the margins.</span></p> 1311 </div> 1312 </body> 1313 </html>