minified/fixed css

Orual 9f507e48 4115b35f

+1859 -225
+347 -6
Cargo.lock
··· 55 55 56 56 [[package]] 57 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" 58 69 version = "0.8.12" 59 70 source = "registry+https://github.com/rust-lang/crates.io-index" 60 71 checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" ··· 675 686 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 676 687 677 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]] 678 698 name = "base64ct" 679 699 version = "1.8.1" 680 700 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 744 764 checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" 745 765 dependencies = [ 746 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", 747 779 ] 748 780 749 781 [[package]] ··· 908 940 ] 909 941 910 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]] 911 965 name = "bytemuck" 912 966 version = "1.24.0" 913 967 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1405 1459 1406 1460 [[package]] 1407 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" 1408 1471 version = "0.4.3" 1409 1472 source = "registry+https://github.com/rust-lang/crates.io-index" 1410 1473 checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" ··· 1414 1477 version = "0.7.1" 1415 1478 source = "registry+https://github.com/rust-lang/crates.io-index" 1416 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 + ] 1417 1491 1418 1492 [[package]] 1419 1493 name = "const_format" ··· 1745 1819 ] 1746 1820 1747 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]] 1748 1844 name = "cssparser-macros" 1749 1845 version = "0.6.1" 1750 1846 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1920 2016 dependencies = [ 1921 2017 "data-encoding", 1922 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", 1923 2028 ] 1924 2029 1925 2030 [[package]] ··· 3735 3840 version = "1.3.0" 3736 3841 source = "registry+https://github.com/rust-lang/crates.io-index" 3737 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" 3738 3849 3739 3850 [[package]] 3740 3851 name = "futf" ··· 4440 4551 version = "0.12.3" 4441 4552 source = "registry+https://github.com/rust-lang/crates.io-index" 4442 4553 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 4554 + dependencies = [ 4555 + "ahash 0.7.8", 4556 + ] 4443 4557 4444 4558 [[package]] 4445 4559 name = "hashbrown" ··· 4447 4561 source = "registry+https://github.com/rust-lang/crates.io-index" 4448 4562 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 4449 4563 dependencies = [ 4450 - "ahash", 4564 + "ahash 0.8.12", 4451 4565 "allocator-api2", 4452 4566 ] 4453 4567 ··· 5535 5649 5536 5650 [[package]] 5537 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" 5538 5661 version = "0.11.0" 5539 5662 source = "registry+https://github.com/rust-lang/crates.io-index" 5540 5663 checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" ··· 5952 6075 source = "registry+https://github.com/rust-lang/crates.io-index" 5953 6076 checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" 5954 6077 dependencies = [ 5955 - "cssparser", 6078 + "cssparser 0.29.6", 5956 6079 "html5ever 0.29.1", 5957 6080 "indexmap 2.12.1", 5958 6081 "selectors", ··· 6105 6228 ] 6106 6229 6107 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]] 6108 6271 name = "linked-hash-map" 6109 6272 version = "0.5.6" 6110 6273 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6621 6784 source = "registry+https://github.com/rust-lang/crates.io-index" 6622 6785 checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" 6623 6786 dependencies = [ 6624 - "ahash", 6787 + "ahash 0.8.12", 6625 6788 "portable-atomic", 6626 6789 ] 6627 6790 ··· 7557 7720 ] 7558 7721 7559 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]] 7560 7729 name = "owo-colors" 7561 7730 version = "4.2.3" 7562 7731 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7610 7779 ] 7611 7780 7612 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]] 7613 7812 name = "parking" 7614 7813 version = "2.2.1" 7615 7814 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 7643 7842 version = "1.0.15" 7644 7843 source = "registry+https://github.com/rust-lang/crates.io-index" 7645 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" 7646 7857 7647 7858 [[package]] 7648 7859 name = "pem-rfc7468" ··· 8274 8485 ] 8275 8486 8276 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]] 8277 8508 name = "publicsuffix" 8278 8509 version = "2.3.0" 8279 8510 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8343 8574 source = "registry+https://github.com/rust-lang/crates.io-index" 8344 8575 checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" 8345 8576 dependencies = [ 8346 - "ahash", 8577 + "ahash 0.8.12", 8347 8578 "equivalent", 8348 8579 "hashbrown 0.16.1", 8349 8580 "parking_lot", ··· 8420 8651 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 8421 8652 8422 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]] 8423 8660 name = "rand" 8424 8661 version = "0.7.3" 8425 8662 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8573 8810 version = "0.6.2" 8574 8811 source = "registry+https://github.com/rust-lang/crates.io-index" 8575 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 + ] 8576 8833 8577 8834 [[package]] 8578 8835 name = "redox_syscall" ··· 8659 8916 checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 8660 8917 8661 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]] 8662 8928 name = "reqwest" 8663 8929 version = "0.12.26" 8664 8930 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 8786 9052 "libc", 8787 9053 "untrusted", 8788 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", 8789 9084 ] 8790 9085 8791 9086 [[package]] ··· 9079 9374 version = "1.2.0" 9080 9375 source = "registry+https://github.com/rust-lang/crates.io-index" 9081 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" 9082 9383 9083 9384 [[package]] 9084 9385 name = "sec1" ··· 9137 9438 checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" 9138 9439 dependencies = [ 9139 9440 "bitflags 1.3.2", 9140 - "cssparser", 9441 + "cssparser 0.29.6", 9141 9442 "derive_more 0.99.20", 9142 9443 "fxhash", 9143 9444 "log", ··· 9183 9484 dependencies = [ 9184 9485 "serde_core", 9185 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", 9186 9496 ] 9187 9497 9188 9498 [[package]] ··· 9590 9900 version = "3.0.0-rc.5" 9591 9901 source = "registry+https://github.com/rust-lang/crates.io-index" 9592 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 + ] 9593 9912 9594 9913 [[package]] 9595 9914 name = "simd-adler32" ··· 10342 10661 ] 10343 10662 10344 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]] 10345 10670 name = "target-lexicon" 10346 10671 version = "0.12.16" 10347 10672 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11303 11628 checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" 11304 11629 dependencies = [ 11305 11630 "base64 0.22.1", 11306 - "data-url", 11631 + "data-url 0.3.2", 11307 11632 "flate2", 11308 11633 "fontdb", 11309 11634 "imagesize", ··· 11378 11703 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 11379 11704 11380 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]] 11381 11712 name = "walkdir" 11382 11713 version = "2.5.0" 11383 11714 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 11670 12001 "jacquard-identity", 11671 12002 "jacquard-lexicon", 11672 12003 "js-sys", 12004 + "lightningcss", 11673 12005 "lol_alloc", 11674 12006 "loro", 11675 12007 "markdown-weaver", ··· 12683 13015 "wasm-bindgen", 12684 13016 "wasm-bindgen-futures", 12685 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", 12686 13027 ] 12687 13028 12688 13029 [[package]]
+4 -1
crates/weaver-app/Cargo.toml
··· 35 35 web = ["dioxus/web", "dioxus-primitives/web"] 36 36 desktop = ["dioxus/desktop"] 37 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"] 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 39 collab-worker = ["weaver-common/iroh"] 40 40 41 41 ··· 87 87 textwrap = { version = "0.16", optional = true } 88 88 askama = { version = "0.12", optional = true } 89 89 fontdb = { version = "0.22", optional = true } 90 + 91 + # CSS minification (server-only) 92 + lightningcss = { version = "1.0.0-alpha.68", optional = true } 90 93 91 94 [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] 92 95 webbrowser = "1.0.6"
+3 -1
crates/weaver-app/assets/styling/entry.css
··· 321 321 border: 1px solid var(--color-border); 322 322 border-radius: 4px; 323 323 text-decoration: none; 324 - transition: color 0.2s ease, border-color 0.2s ease; 324 + transition: 325 + color 0.2s ease, 326 + border-color 0.2s ease; 325 327 } 326 328 327 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 88 89 89 #[component] 90 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! {}, 91 + rsx! { 92 + document::Stylesheet { href: asset!("/assets/styling/theme-defaults.css") } 93 + document::Stylesheet { href: asset!("/assets/styling/notebook-defaults.css") } 110 94 } 111 95 } 112 96 ··· 155 139 .unwrap_or_default(), 156 140 ); 157 141 142 + let css = minify_css(&css).unwrap_or(css); 143 + 158 144 Ok(([(CONTENT_TYPE, "text/css")], css).into_response()) 159 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 1585 ident: ReadSignal<AtIdentifier<'static>>, 1586 1586 rkey: ReadSignal<SmolStr>, 1587 1587 ) -> ( 1588 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>)>>, RenderError>, 1588 + Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1589 1589 Memo<Option<crate::fetch::LeafletDocumentData>>, 1590 1590 ) { 1591 1591 use weaver_api::pub_leaflet::document::Document; ··· 1595 1595 let res = use_server_future(move || { 1596 1596 let fetcher = fetcher.clone(); 1597 1597 async move { 1598 + use jacquard::IntoStatic; 1598 1599 use jacquard::client::AgentSessionExt; 1600 + use jacquard::prelude::IdentityResolver; 1601 + use weaver_api::pub_leaflet::document::DocumentPagesItem; 1599 1602 use weaver_api::pub_leaflet::publication::Publication; 1603 + use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1600 1604 1601 1605 let ident = ident(); 1602 1606 let rkey = rkey(); ··· 1646 1650 None 1647 1651 }; 1648 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 + 1649 1677 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1650 1678 1651 1679 Some(( 1652 1680 serde_json::to_value(&record.value).ok()?, 1653 1681 serde_json::to_value(&*profile).ok()?, 1654 1682 publication_base_path, 1683 + rendered_html, 1655 1684 )) 1656 1685 } 1657 1686 }); ··· 1661 1690 use weaver_api::sh_weaver::actor::ProfileDataView; 1662 1691 1663 1692 let res = res.as_ref().ok()?; 1664 - if let Some(Some((doc_json, profile_json, base_path))) = &*res.read() { 1693 + if let Some(Some((doc_json, profile_json, base_path, rendered_html))) = &*res.read() { 1665 1694 let document = jacquard::from_json_value::<Document>(doc_json.clone()).ok()?; 1666 1695 let profile = 1667 1696 jacquard::from_json_value::<ProfileDataView>(profile_json.clone()).ok()?; ··· 1669 1698 document, 1670 1699 profile, 1671 1700 publication_base_path: base_path.clone(), 1701 + rendered_html: rendered_html.clone(), 1672 1702 }) 1673 1703 } else { 1674 1704 None ··· 1687 1717 Memo<Option<crate::fetch::LeafletDocumentData>>, 1688 1718 ) { 1689 1719 use jacquard::IntoStatic; 1690 - use weaver_api::pub_leaflet::document::Document; 1720 + use jacquard::prelude::IdentityResolver; 1721 + use weaver_api::pub_leaflet::document::{Document, DocumentPagesItem}; 1691 1722 use weaver_api::pub_leaflet::publication::Publication; 1723 + use weaver_renderer::leaflet::{LeafletRenderContext, render_linear_document}; 1692 1724 1693 1725 let fetcher = use_context::<crate::fetch::Fetcher>(); 1694 1726 ··· 1730 1762 None 1731 1763 }; 1732 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 + 1733 1789 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1734 1790 1735 1791 Some(crate::fetch::LeafletDocumentData { 1736 1792 document: record.value.into_static(), 1737 1793 profile: (*profile).clone(), 1738 1794 publication_base_path, 1795 + rendered_html, 1739 1796 }) 1740 1797 } 1741 1798 }); ··· 1753 1810 ident: ReadSignal<AtIdentifier<'static>>, 1754 1811 rkey: ReadSignal<SmolStr>, 1755 1812 ) -> ( 1756 - Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>)>>, RenderError>, 1813 + Result<Resource<Option<(serde_json::Value, serde_json::Value, Option<String>, Option<String>)>>, RenderError>, 1757 1814 Memo<Option<crate::fetch::PcktDocumentData>>, 1758 1815 ) { 1759 1816 let fetcher = use_context::<crate::fetch::Fetcher>(); ··· 1761 1818 let res = use_server_future(move || { 1762 1819 let fetcher = fetcher.clone(); 1763 1820 async move { 1821 + use jacquard::IntoStatic; 1764 1822 use jacquard::client::AgentSessionExt; 1823 + use jacquard::prelude::IdentityResolver; 1765 1824 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1766 1825 use weaver_api::site_standard::publication::Publication; 1826 + use weaver_renderer::pckt::{PcktRenderContext, render_content_blocks}; 1767 1827 1768 1828 let ident = ident(); 1769 1829 let rkey = rkey(); ··· 1807 1867 None 1808 1868 }; 1809 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 + 1810 1884 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1811 1885 1812 1886 Some(( 1813 1887 serde_json::to_value(&doc).ok()?, 1814 1888 serde_json::to_value(&*profile).ok()?, 1815 1889 publication_url, 1890 + rendered_html, 1816 1891 )) 1817 1892 } 1818 1893 }); ··· 1822 1897 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1823 1898 1824 1899 let res = res.as_ref().ok()?; 1825 - if let Some(Some((doc_json, profile_json, publication_url))) = &*res.read() { 1900 + if let Some(Some((doc_json, profile_json, publication_url, rendered_html))) = &*res.read() { 1826 1901 let document = 1827 1902 jacquard::from_json_value::<SiteStandardDocument>(doc_json.clone()).ok()?; 1828 1903 let profile = ··· 1831 1906 document, 1832 1907 profile, 1833 1908 publication_url: publication_url.clone(), 1909 + rendered_html: rendered_html.clone(), 1834 1910 }) 1835 1911 } else { 1836 1912 None ··· 1849 1925 Memo<Option<crate::fetch::PcktDocumentData>>, 1850 1926 ) { 1851 1927 use jacquard::IntoStatic; 1928 + use jacquard::prelude::IdentityResolver; 1852 1929 use weaver_api::site_standard::document::Document as SiteStandardDocument; 1853 1930 use weaver_api::site_standard::publication::Publication; 1931 + use weaver_renderer::pckt::{PcktRenderContext, render_content_blocks}; 1854 1932 1855 1933 let fetcher = use_context::<crate::fetch::Fetcher>(); 1856 1934 ··· 1899 1977 None 1900 1978 }; 1901 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 + 1902 1994 let profile = fetcher.fetch_profile(&ident).await.ok()?; 1903 1995 1904 1996 Some(crate::fetch::PcktDocumentData { 1905 1997 document: doc, 1906 1998 profile: (*profile).clone(), 1907 1999 publication_url, 2000 + rendered_html, 1908 2001 }) 1909 2002 } 1910 2003 });
+4
crates/weaver-app/src/fetch.rs
··· 90 90 pub profile: ProfileDataView<'static>, 91 91 /// Publication base_path for constructing external URL (e.g., "connectedplaces.leaflet.pub") 92 92 pub publication_base_path: Option<String>, 93 + /// Pre-rendered HTML content 94 + pub rendered_html: Option<String>, 93 95 } 94 96 95 97 /// Data for a site.standard / blog.pckt document ··· 100 102 pub profile: ProfileDataView<'static>, 101 103 /// Publication URL for constructing external URL (e.g., "https://crypto.pckt.blog") 102 104 pub publication_url: Option<String>, 105 + /// Pre-rendered HTML content 106 + pub rendered_html: Option<String>, 103 107 } 104 108 105 109 pub struct Client {
+3 -3
crates/weaver-app/src/views/entry.rs
··· 77 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } 78 78 NotebookCss { ident: ident().to_smolstr(), notebook: book_title.clone() } 79 79 80 - div { class: "entry-page-layout", 80 + div { class: "entry-page", 81 81 if let Some(ref prev) = book_entry_view.prev { 82 82 div { class: "nav-gutter nav-prev", 83 83 NavButton { ··· 133 133 DefaultNotebookCss {} 134 134 135 135 136 - div { class: "entry-page-layout", 136 + div { class: "entry-page", 137 137 div { class: "entry-content-main notebook-content", 138 138 { 139 139 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); ··· 234 234 document::Link { rel: "stylesheet", href: ENTRY_CSS } 235 235 NotebookCss { ident: ident().to_smolstr(), notebook: book_title() } 236 236 237 - div { class: "entry-page-layout", 237 + div { class: "entry-page", 238 238 if let Some(ref prev) = book_entry_view.prev { 239 239 div { class: "nav-gutter nav-prev", 240 240 NavButton {
+17 -127
crates/weaver-app/src/views/external.rs
··· 7 7 8 8 use crate::components::css::DefaultNotebookCss; 9 9 use crate::components::{AuthorList, extract_author_info}; 10 - use crate::fetch::Fetcher; 11 10 12 11 #[component] 13 12 pub fn WhiteWindEntry( ··· 71 70 document::Link { rel: "stylesheet", href: ENTRY_CSS } 72 71 DefaultNotebookCss {} 73 72 74 - div { class: "entry-page-layout", 73 + div { class: "entry-page", 75 74 div { class: "entry-content-main notebook-content", 76 75 header { class: "entry-metadata", 77 76 div { class: "entry-header-row", ··· 141 140 rkey: ReadSignal<SmolStr>, 142 141 ) -> Element { 143 142 use crate::components::{ENTRY_CSS, EntryOgMeta}; 144 - use weaver_api::pub_leaflet::document::DocumentPagesItem; 145 143 146 144 let (entry_res, entry_data) = crate::data::use_leaflet_document_data(ident, rkey); 147 145 ··· 170 168 .record(data.profile.clone()) 171 169 .build(); 172 170 173 - let pages = data.document.pages.clone(); 174 - let author_did = data.document.author.clone(); 175 - 176 171 rsx! { 177 172 EntryOgMeta { 178 173 title: title.to_string(), ··· 184 179 document::Link { rel: "stylesheet", href: ENTRY_CSS } 185 180 DefaultNotebookCss {} 186 181 187 - div { class: "entry-page-layout", 182 + div { class: "entry-page", 188 183 div { class: "entry-content-main notebook-content", 189 184 header { class: "entry-metadata", 190 185 div { class: "entry-header-row", ··· 206 201 } 207 202 } 208 203 } 209 - LeafletContent { 210 - pages: pages, 211 - author_did: author_did, 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..." } 212 211 } 213 212 } 214 213 } ··· 218 217 } 219 218 } 220 219 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 220 #[cfg(feature = "pckt")] 285 221 #[component] 286 222 pub fn PcktEntry(ident: ReadSignal<AtIdentifier<'static>>, rkey: ReadSignal<SmolStr>) -> Element { ··· 327 263 .format("%B %d, %Y") 328 264 .to_string(); 329 265 330 - let content = data.document.content.clone(); 331 - let author_did = ident(); 332 - 333 266 // Build external URL from publication URL + path (or rkey) 334 267 let doc_path = data 335 268 .document ··· 349 282 document::Link { rel: "stylesheet", href: ENTRY_CSS } 350 283 DefaultNotebookCss {} 351 284 352 - div { class: "entry-page-layout", 285 + div { class: "entry-page", 353 286 div { class: "entry-content-main notebook-content", 354 287 header { class: "entry-metadata", 355 288 div { class: "entry-header-row", ··· 379 312 } 380 313 } 381 314 } 382 - PcktContent { 383 - content: content, 384 - author_did: author_did, 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..." } 385 322 } 386 323 } 387 324 } 388 325 } 389 326 } 390 327 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 328 } 439 329 } 440 330
+1 -2
crates/weaver-renderer/src/css.rs
··· 132 132 /* When sidenotes exist, body padding creates the gutter */ 133 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 134 body:has(.sidenote) {{ 135 - padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 135 + padding-inline-start: clamp(1rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 136 padding-inline-end: 15.5rem; 137 137 }} 138 138 ··· 260 260 font-size: 0.95em; 261 261 border-bottom-right-radius: 5px; 262 262 border-top-right-radius: 5px; 263 - }} 264 263 }} 265 264 266 265 /* Tables */
+95 -61
test-sidenotes.html
··· 84 84 /* When sidenotes exist, body padding creates the gutter */ 85 85 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 86 86 body:has(.sidenote) { 87 - padding-left: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 88 - padding-right: 15.5rem; 87 + padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 88 + padding-inline-end: 15.5rem; 89 89 } 90 90 91 91 /* Typography */ ··· 154 154 155 155 /* Lists */ 156 156 ul, ol { 157 - margin-left: 1rem; 157 + margin-inline-start: 1rem; 158 158 margin-bottom: 1rem; 159 159 } 160 160 ··· 202 202 203 203 /* Blockquotes */ 204 204 blockquote { 205 - border-left: 2px solid var(--color-secondary); 205 + border-inline-start: 2px solid var(--color-secondary); 206 206 background: var(--color-surface); 207 - padding-left: 1rem; 208 - padding-right: 1rem; 207 + padding-inline-start: 1rem; 208 + padding-inline-end: 1rem; 209 209 padding-top: 0.5rem; 210 210 padding-bottom: 0.04rem; 211 211 margin: 1rem 0; ··· 228 228 th, td { 229 229 border: 1px solid var(--color-border); 230 230 padding: 0.5rem; 231 - text-align: left; 231 + text-align: start; 232 232 } 233 233 234 234 th { ··· 270 270 271 271 .footnote-definition-label { 272 272 font-weight: 600; 273 - margin-right: 0.5rem; 273 + margin-inline-end: 0.5rem; 274 274 color: var(--color-primary); 275 275 } 276 276 277 277 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 278 278 .notebook-content aside, 279 279 .notebook-content .aside { 280 - float: left; 280 + float: inline-start; 281 281 width: 40%; 282 282 margin: 0 1.5rem 1rem 0; 283 283 padding: 1rem; 284 284 background: var(--color-surface); 285 - border-right: 3px solid var(--color-primary); 285 + border-inline-end: 3px solid var(--color-primary); 286 286 font-size: 0.9em; 287 - clear: left; 287 + clear: inline-start; 288 288 } 289 289 290 290 .notebook-content aside > *:first-child, ··· 300 300 /* Reset blockquote styling inside asides */ 301 301 .notebook-content aside > blockquote, 302 302 .notebook-content .aside > blockquote { 303 - border-left: none; 303 + border-inline-start: none; 304 304 background: transparent; 305 305 padding: 0; 306 306 margin: 0; ··· 308 308 } 309 309 310 310 /* Indent utilities */ 311 - .indent-1 { margin-left: 1em; } 312 - .indent-2 { margin-left: 2em; } 313 - .indent-3 { margin-left: 3em; } 311 + .indent-1 { margin-inline-start: 1em; } 312 + .indent-2 { margin-inline-start: 2em; } 313 + .indent-3 { margin-inline-start: 3em; } 314 314 315 315 /* Tufte-style Sidenotes */ 316 316 /* Hide checkbox for sidenote toggle */ ··· 329 329 position: relative; 330 330 top: -0.5em; 331 331 color: var(--color-primary); 332 - padding-left: 0.1em; 332 + padding-inline-start: 0.1em; 333 333 } 334 334 335 335 /* Sidenote content (margin notes on wide screens) */ 336 336 .sidenote { 337 - float: right; 338 - clear: right; 339 - margin-right: -15.5rem; 337 + float: inline-end; 338 + clear: inline-end; 339 + margin-inline-end: -15.5rem; 340 340 width: 14rem; 341 341 margin-top: 0.3rem; 342 342 margin-bottom: 1rem; ··· 354 354 @media (max-width: 900px) { 355 355 /* Reset sidenote gutter on mobile */ 356 356 body:has(.sidenote) { 357 - padding-right: 0; 357 + padding-inline-end: 0; 358 358 } 359 359 360 360 aside, .aside { ··· 374 374 margin: 0.5rem 2.5%; 375 375 padding: 0.5rem; 376 376 background: var(--color-surface); 377 - border-left: 2px solid var(--color-primary); 377 + border-inline-start: 2px solid var(--color-primary); 378 378 } 379 379 380 380 label.sidenote-number { ··· 412 412 margin: 1rem 0; 413 413 padding: 1rem; 414 414 background: var(--color-surface); 415 - border-left: 2px solid var(--color-secondary); 415 + border-inline-start: 2px solid var(--color-secondary); 416 416 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 417 417 } 418 418 419 419 .atproto-embed:hover { 420 - border-left-color: var(--color-primary); 420 + border-inline-start-color: var(--color-primary); 421 421 } 422 422 423 423 @media (prefers-color-scheme: dark) { 424 424 .atproto-embed { 425 425 box-shadow: none; 426 426 border: 1px solid var(--color-border); 427 - border-left: 2px solid var(--color-secondary); 427 + border-inline-start: 2px solid var(--color-secondary); 428 428 } 429 429 } 430 430 ··· 601 601 } 602 602 603 603 .embed-external:hover { 604 - border-left: 2px solid var(--color-primary); 605 - margin-left: -1px; 604 + border-inline-start: 2px solid var(--color-primary); 605 + margin-inline-start: -1px; 606 606 } 607 607 608 608 @media (prefers-color-scheme: dark) { ··· 611 611 } 612 612 613 613 .embed-external:hover { 614 - border-left: 2px solid var(--color-primary); 615 - margin-left: -1px; 614 + border-inline-start: 2px solid var(--color-primary); 615 + margin-inline-start: -1px; 616 616 } 617 617 } 618 618 ··· 698 698 margin-top: 0.5rem; 699 699 padding: 0.75rem; 700 700 background: var(--color-overlay); 701 - border-left: 2px solid var(--color-tertiary); 701 + border-inline-start: 2px solid var(--color-tertiary); 702 702 } 703 703 704 704 @media (prefers-color-scheme: dark) { 705 705 .embed-quote { 706 706 border: 1px solid var(--color-border); 707 - border-left: 2px solid var(--color-tertiary); 707 + border-inline-start: 2px solid var(--color-tertiary); 708 708 } 709 709 } 710 710 ··· 735 735 display: block; 736 736 padding: 1rem; 737 737 background: var(--color-overlay); 738 - border-left: 2px solid var(--color-border); 738 + border-inline-start: 2px solid var(--color-border); 739 739 color: var(--color-muted); 740 740 font-style: italic; 741 741 margin-top: 0.5rem; ··· 759 759 margin-top: 0.5rem; 760 760 padding: 0.75rem; 761 761 background: var(--color-overlay); 762 - border-left: 2px solid var(--color-tertiary); 762 + border-inline-start: 2px solid var(--color-tertiary); 763 763 } 764 764 765 765 .embed-record-card > .embed-author-name { ··· 802 802 .embed-fields .embed-fields { 803 803 display: block; 804 804 margin-top: 0.5rem; 805 - margin-left: 1rem; 806 - padding-left: 0.5rem; 807 - border-left: 1px solid var(--color-border); 805 + margin-inline-start: 1rem; 806 + padding-inline-start: 0.5rem; 807 + border-inline-start: 1px solid var(--color-border); 808 808 } 809 809 810 810 /* Type label inside fields should be block with spacing */ ··· 919 919 padding: 0; 920 920 background: var(--color-surface); 921 921 border: 1px solid var(--color-border); 922 - border-left: 1px solid var(--color-border); 922 + border-inline-start: 1px solid var(--color-border); 923 923 box-shadow: none; 924 924 overflow: hidden; 925 925 } 926 926 927 927 .atproto-entry:hover { 928 - border-left-color: var(--color-border); 928 + border-inline-start-color: var(--color-border); 929 929 } 930 930 931 931 @media (prefers-color-scheme: dark) { 932 932 .atproto-entry { 933 933 border: 1px solid var(--color-border); 934 - border-left: 1px solid var(--color-border); 934 + border-inline-start: 1px solid var(--color-border); 935 935 } 936 936 } 937 937 ··· 1027 1027 h3 { font-size: 1.2rem; } 1028 1028 1029 1029 blockquote { 1030 - margin-left: 0; 1031 - margin-right: 0; 1030 + margin-inline-start: 0; 1031 + margin-inline-end: 0; 1032 1032 } 1033 1033 } 1034 1034 ··· 1043 1043 h3 { font-size: 1.1rem; } 1044 1044 1045 1045 blockquote { 1046 - padding-left: 0.75rem; 1047 - padding-right: 0.75rem; 1046 + padding-inline-start: 0.75rem; 1047 + padding-inline-end: 0.75rem; 1048 1048 } 1049 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; } 1050 1084 </style> 1051 1085 <style> 1052 1086 /* Syntax highlighting - Light Mode (default) */ ··· 1236 1270 <body style="background: var(--color-base); min-height: 100vh;"> 1237 1271 <div class="notebook-content"> 1238 1272 <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> 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> 1241 1275 <blockquote> 1242 - <p><img src="weaver_photo_med.jpg" alt="weaver_photo_med.jpg" /><em>The namesake of what I'm building</em></p> 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> 1243 1277 </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> 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> 1245 1279 <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> 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> 1247 1281 <aside> 1248 1282 <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> 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> 1251 1285 </blockquote> 1252 1286 </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> 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> 1255 1289 <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> 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> 1257 1291 <aside> 1258 1292 <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> 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> 1261 1295 </blockquote> 1262 1296 </aside> 1263 1297 <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> 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> 1266 1300 <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> 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> 1268 1302 <aside> 1269 1303 <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> 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> 1272 1306 </blockquote> 1273 1307 </aside> 1274 1308 <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> 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> 1277 1311 </div> 1278 1312 </body> 1279 1313 </html>