swaped favicon, bunch of little additions.

Orual 27e5411d f219fec8

+601 -95
+8 -8
Cargo.lock
··· 3951 [[package]] 3952 name = "jacquard" 3953 version = "0.8.0" 3954 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 3955 dependencies = [ 3956 "bytes", 3957 "getrandom 0.2.16", ··· 3980 [[package]] 3981 name = "jacquard-api" 3982 version = "0.8.0" 3983 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 3984 dependencies = [ 3985 "bon", 3986 "bytes", ··· 3998 [[package]] 3999 name = "jacquard-axum" 4000 version = "0.8.0" 4001 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4002 dependencies = [ 4003 "axum", 4004 "bytes", ··· 4020 [[package]] 4021 name = "jacquard-common" 4022 version = "0.8.0" 4023 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4024 dependencies = [ 4025 "base64 0.22.1", 4026 "bon", ··· 4062 [[package]] 4063 name = "jacquard-derive" 4064 version = "0.8.0" 4065 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4066 dependencies = [ 4067 "heck 0.5.0", 4068 "jacquard-lexicon", ··· 4074 [[package]] 4075 name = "jacquard-identity" 4076 version = "0.8.0" 4077 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4078 dependencies = [ 4079 "bon", 4080 "bytes", ··· 4101 [[package]] 4102 name = "jacquard-lexicon" 4103 version = "0.8.0" 4104 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4105 dependencies = [ 4106 "cid", 4107 "dashmap", ··· 4127 [[package]] 4128 name = "jacquard-oauth" 4129 version = "0.8.0" 4130 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#71aae4e8c9a174927d3404fdcf9031547ca89691" 4131 dependencies = [ 4132 "base64 0.22.1", 4133 "bytes",
··· 3951 [[package]] 3952 name = "jacquard" 3953 version = "0.8.0" 3954 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 3955 dependencies = [ 3956 "bytes", 3957 "getrandom 0.2.16", ··· 3980 [[package]] 3981 name = "jacquard-api" 3982 version = "0.8.0" 3983 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 3984 dependencies = [ 3985 "bon", 3986 "bytes", ··· 3998 [[package]] 3999 name = "jacquard-axum" 4000 version = "0.8.0" 4001 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4002 dependencies = [ 4003 "axum", 4004 "bytes", ··· 4020 [[package]] 4021 name = "jacquard-common" 4022 version = "0.8.0" 4023 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4024 dependencies = [ 4025 "base64 0.22.1", 4026 "bon", ··· 4062 [[package]] 4063 name = "jacquard-derive" 4064 version = "0.8.0" 4065 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4066 dependencies = [ 4067 "heck 0.5.0", 4068 "jacquard-lexicon", ··· 4074 [[package]] 4075 name = "jacquard-identity" 4076 version = "0.8.0" 4077 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4078 dependencies = [ 4079 "bon", 4080 "bytes", ··· 4101 [[package]] 4102 name = "jacquard-lexicon" 4103 version = "0.8.0" 4104 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4105 dependencies = [ 4106 "cid", 4107 "dashmap", ··· 4127 [[package]] 4128 name = "jacquard-oauth" 4129 version = "0.8.0" 4130 + source = "git+https://tangled.org/@nonbinary.computer/jacquard#ee0f31db86814449fa586aa6b1abf65a52733f9e" 4131 dependencies = [ 4132 "base64 0.22.1", 4133 "bytes",
crates/weaver-server/assets/favicon.ico

This is a binary file and will not be displayed.

-20
crates/weaver-server/assets/header.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1007 197"><style> 2 - @keyframes a0_t { 0% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } 35% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { transform: translate(225px,339.9px) scale(1,1) translate(-89.5px,-24px); } 50% { transform: translate(225px,339.9px) scale(1,1) translate(-89.5px,-24px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } 100% { transform: translate(225px,339.9px) scale(0,1) translate(-89.5px,-24px); } } 3 - @keyframes a1_t { 0% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } 20% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,348.9px) scale(1,1) translate(-41.5px,-11px); } 35% { transform: translate(225px,348.9px) scale(1,1) translate(-41.5px,-11px); } 45% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } 100% { transform: translate(225px,348.9px) scale(0,1) translate(-41.5px,-11px); } } 4 - @keyframes a3_t { 0% { transform: translate(225px,198.9px) rotate(-90deg); } 10% { transform: translate(225px,198.9px) rotate(-90deg); } 20% { transform: translate(225px,198.9px) rotate(-90deg); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,198.9px) rotate(0deg); } 35% { transform: translate(225px,198.9px) rotate(0deg); } 45% { transform: translate(225px,198.9px) rotate(0deg); } 50% { transform: translate(225px,198.9px) rotate(0deg); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(225px,198.9px) rotate(-90deg); } 65% { transform: translate(225px,198.9px) rotate(-90deg); } 75% { transform: translate(225px,198.9px) rotate(-90deg); } 100% { transform: translate(225px,198.9px) rotate(-90deg); } } 5 - @keyframes a2_t { 0% { transform: translate(-200px,-167px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(-150px,-80px); } 20% { transform: translate(-150px,-80px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(-200px,-123px); } 35% { transform: translate(-200px,-123px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { transform: translate(-150px,-100px); } 50% { transform: translate(-150px,-100px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(-150px,-80px); } 65% { transform: translate(-150px,-80px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { transform: translate(-217px,-200px); } 100% { transform: translate(-217px,-200px); } } 6 - @keyframes a2_o { 0% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { opacity: 1; } 65% { opacity: 1; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { opacity: 0; } 100% { opacity: 0; } } 7 - @keyframes a2_sw { 0% { stroke-width: 30px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { stroke-width: 8px; } 65% { stroke-width: 8px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { stroke-width: 30px; } 100% { stroke-width: 30px; } } 8 - @keyframes a2_w { 0% { width: 400px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { width: 300px; } 20% { width: 300px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { width: 400px; } 35% { width: 400px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { width: 300px; } 50% { width: 300px; } 60% { width: 300px; } 65% { width: 300px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { width: 434px; } 100% { width: 434px; } } 9 - @keyframes a2_h { 0% { height: 334px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { height: 160px; } 20% { height: 160px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { height: 246px; } 35% { height: 246px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 45% { height: 200px; } 50% { height: 200px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { height: 160px; } 65% { height: 160px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { height: 400px; } 100% { height: 400px; } } 10 - @keyframes a4_t { 0% { transform: translate(143.5px,48.9px); } 10% { transform: translate(180.5px,98.9px); } 100% { transform: translate(180.5px,98.9px); } } 11 - @keyframes a4_w { 0% { width: 168.5px; } 10% { width: 94.5px; } 100% { width: 94.5px; } } 12 - @keyframes a4_h { 0% { height: 302px; } 10% { height: 202px; } 100% { height: 202px; } } 13 - @keyframes a5_t { 0% { transform: translate(217px,324.9px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(190px,324.9px); } 20% { transform: translate(190px,324.9px); } 30% { transform: translate(190px,324.9px); } 100% { transform: translate(190px,324.9px); } } 14 - @keyframes a5_o { 0% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { opacity: 1; } 20% { opacity: 1; } 30% { opacity: 0; } 100% { opacity: 0; } } 15 - @keyframes a5_w { 0% { width: 16px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { width: 70px; } 20% { width: 70px; } 30% { width: 70px; } 100% { width: 70px; } } 16 - @keyframes a6_t { 0% { transform: translate(217px,324.9px); } 50% { transform: translate(217px,324.9px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { transform: translate(190px,324.9px); } 65% { transform: translate(190px,324.9px); } 75% { transform: translate(190px,324.9px); } 100% { transform: translate(190px,324.9px); } } 17 - @keyframes a6_o { 0% { opacity: 0; } 50% { opacity: 0; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { opacity: 1; } 65% { opacity: 1; animation-timing-function: cubic-bezier(.6,0,.4,1); } 75% { opacity: 0; } 100% { opacity: 0; } } 18 - @keyframes a6_w { 0% { width: 16px; } 50% { width: 16px; animation-timing-function: cubic-bezier(.6,0,.4,1); } 60% { width: 70px; } 65% { width: 70px; } 75% { width: 70px; } 100% { width: 70px; } } 19 - @keyframes a7_t { 0% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 10% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 20% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 25% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.6,0,.4,1); } 30% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 35% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 45% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); } 50% { transform: translate(225px,198.9px) scale(.8,.8) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 55% { transform: translate(225px,198.9px) scale(.7,.7) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 60% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); } 65% { transform: translate(225px,198.9px) scale(1,1) translate(-42.3px,-93px); animation-timing-function: cubic-bezier(.4,0,.6,1); } 75% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); } 100% { transform: translate(225px,198.9px) scale(1.5,1.5) translate(-42.3px,-93px); } } 20 - </style><defs><symbol id="Symbol-2" preserveAspectRatio="none" width="111.5" height="186" viewBox="0 0 111.5 186" overflow="visible"><g transform="translate(8.5,4.5)"><rect x="4" width="85" height="8" fill="#d9d9d9"/><rect x="4" y="26" width="85" height="8" fill="#d9d9d9"/><rect x="25" y="56" width="43" height="8" fill="#d9d9d9"/><rect width="85" height="8" fill="#d9d9d9" transform="translate(46.5,175) rotate(180) scale(-1,1) translate(-42.5,-4)"/><rect width="85" height="8" fill="#d9d9d9" transform="translate(46.5,149) rotate(180) scale(-1,1) translate(-42.5,-4)"/><rect width="43" height="8" fill="#d9d9d9" transform="translate(46.5,119) rotate(180) scale(-1,1) translate(-21.5,-4)"/><path d="M0 4c0 53 94.5 115 94.5 169" stroke="#3cc4dc" stroke-width="17" stroke-linecap="square" fill="none"/><path d="M94.5 4c0 53-94.5 115-94.5 169" stroke="#fb422d" stroke-width="17" stroke-linecap="square" fill="none"/></g></symbol><symbol id="Symbol-5" preserveAspectRatio="none" width="84.5" height="186" viewBox="0 0 84.5 186" overflow="visible"><g transform="translate(42.3,93) scale(5.352,6.9) translate(-16,60)"><g transform="translate(-34.9,-125.8)"><g transform="translate(24,5)"><path d="M33 46.5c0 3.1-0.8 5.5-2.2 7.4c-1.4 1.9-3.3 3.6-5.4 5.4c-2.2 1.7-4.5 3.6-6.3 6.2c-1.8 2.5-3.1 5.5-3.1 9.5h4.7c0-3.1 .9-5.4 2.2-7.3c1.4-1.9 3.3-3.5 5.5-5.3c2.1-1.8 4.4-3.7 6.2-6.2c1.8-2.6 3.1-5.6 3.1-9.7Z" fill="#e96020"/><path d="M20.4 70.7c-0.6 0-1 .5-1 1.1c0 .7 .4 1.2 1 1.2h12.9c.7 0 1.2-0.5 1.2-1.2c0-0.6-0.5-1.1-1.2-1.1Z" fill="#2d323b"/><path d="M21.8 66.6c-0.7 0-1 .6-1 1.2c0 .6 .3 1 .9 1h10.3c.6 0 1.1-0.5 1.1-1.1c0-0.6-0.5-1.1-1.1-1.1Z" fill="#2d323b"/><path d="M21.8 53c-0.7 0-1.2 .5-1.1 1.1c0 .6 .4 1.1 1.1 1.1h10.2c.6 0 .9-0.6 .9-1.2c0-0.6-0.3-1-0.9-1Z" fill="#2d323b"/><path d="M20.4 48.8c-0.6 0-1.2 .5-1.2 1.1c0 .7 .6 1.2 1.2 1.2h12.9c.7 0 1-0.5 1-1.1c0-0.7-0.3-1.2-1-1.2Z" fill="#2d323b"/><path d="M16 46.5c0 4.1 1.3 7.1 3.1 9.7c1.8 2.5 4.1 4.4 6.3 6.2c2.1 1.8 4 3.6 5.4 5.5c1.4 1.9 2.2 4 2.2 7.1h4.7c0-4-1.3-7.1-3.1-9.6c-1.8-2.5-4.1-4.4-6.2-6.2c-2.2-1.8-4.1-3.4-5.5-5.3c-1.3-1.9-2.2-4.3-2.2-7.4Z" fill="#00a8d6"/></g></g></g></symbol></defs><rect width="1007" height="197" stroke="#0f1116" fill="#0f1116" stroke-width="0"/><path fill="#fff" d="M83.3 106.1v-65.7h17c4.2 0 7.9 .8 11 2.4c3.1 1.6 5.5 3.9 7.2 6.9c1.7 2.9 2.6 6.4 2.6 10.4v26.2c0 4-0.9 7.5-2.6 10.5c-1.7 3-4.1 5.3-7.2 6.9c-3.1 1.6-6.8 2.4-11 2.4Zm8.1-7.2h8.9c4 0 7.1-1.1 9.3-3.3c2.3-2.3 3.4-5.3 3.4-9.3v-26.2c0-3.9-1.1-7-3.4-9.2c-2.2-2.2-5.3-3.3-9.3-3.3h-8.9Zm45.3 7.2v-7.4h17.5v-34.7h-15.7v-7.4h23.8v42.1h16.7v7.4Zm20.7-58.4c-2 0-3.6-0.5-4.7-1.5c-1.2-1.1-1.7-2.4-1.7-4.2c0-1.8 .5-3.2 1.7-4.3c1.1-1 2.7-1.5 4.7-1.5c1.9 0 3.5 .5 4.6 1.5c1.2 1.1 1.7 2.5 1.7 4.3c0 1.8-0.5 3.1-1.7 4.2c-1.1 1-2.7 1.5-4.6 1.5Zm52.6 59.1c-6 0-10.7-1.7-14.2-5.1c-3.4-3.4-5.1-8-5.1-13.8v-13.1c0-5.9 1.7-10.5 5.1-13.9c3.5-3.3 8.2-5 14.2-5c6 0 10.7 1.7 14.2 5c3.4 3.4 5.2 8 5.2 13.8v13.2c0 5.8-1.8 10.4-5.2 13.8c-3.5 3.4-8.2 5.1-14.2 5.1Zm0-7.2c3.5 0 6.3-1 8.3-3c2-1.9 3-4.8 3-8.7v-13.1c0-3.9-1-6.8-3-8.8c-2-1.9-4.8-2.9-8.3-2.9c-3.5 0-6.2 1-8.2 2.9c-2 2-3 4.9-3 8.8v13.1c0 3.9 1 6.8 3 8.7c2 2 4.7 3 8.2 3Zm31.5 6.5l17.6-25.5l-16.5-24h9.5l9.8 15.3c.5 .7 .9 1.4 1.2 2.2c.4 .7 .8 1.4 1 1.8c.2-0.4 .5-1.1 .8-1.8c.4-0.8 .8-1.5 1.3-2.2l9.9-15.3h9.4l-16.5 24.1l17.5 25.4h-9.5l-10.7-16.2c-0.4-0.7-0.8-1.4-1.2-2.3c-0.4-0.9-0.7-1.6-1-2.1c-0.2 .5-0.6 1.2-1 2.1c-0.5 .9-1 1.6-1.4 2.3l-10.7 16.2Zm76.4 .9c-5.7 0-10.2-1.7-13.7-5.1c-3.4-3.4-5.1-8-5.1-13.8v-31.5h8.1v31.5c0 3.7 1 6.6 2.9 8.7c1.9 2.1 4.5 3.1 7.8 3.1c3.4 0 6-1 8-3.1c1.9-2.1 2.9-5 2.9-8.7v-31.5h8.1v31.5c0 5.8-1.7 10.4-5.2 13.8c-3.5 3.4-8.1 5.1-13.8 5.1Zm52.6-0.2c-5.1 0-9.3-1.2-12.5-3.5c-3.3-2.3-4.9-5.5-4.9-9.6h8.3c0 1.8 .9 3.3 2.6 4.3c1.8 1.1 4 1.6 6.6 1.6h3.8c3.2 0 5.6-0.6 7.1-1.9c1.6-1.3 2.4-3 2.4-5.2c0-2.1-0.8-3.8-2.2-5c-1.5-1.2-3.6-2-6.4-2.5l-6.6-1c-5-0.9-8.6-2.4-10.9-4.7c-2.4-2.3-3.5-5.5-3.5-9.5c0-4.5 1.4-7.9 4.3-10.3c2.9-2.4 7.1-3.6 12.6-3.6h3.4c5 0 9.1 1.1 12.1 3.4c3 2.2 4.6 5.2 4.6 8.9h-8.3c0-1.6-0.8-2.8-2.3-3.7c-1.5-1-3.6-1.4-6.3-1.4h-3.4c-2.8 0-5 .6-6.5 1.7c-1.5 1.2-2.3 2.9-2.3 5c0 3.6 2.5 5.8 7.6 6.7l6.7 1.1c5.3 .8 9.2 2.4 11.7 4.6c2.4 2.3 3.6 5.6 3.6 9.9c0 4.6-1.5 8.2-4.4 10.8c-2.9 2.6-7.3 3.9-13.2 3.9Z"/><path fill="#fff" d="M91 164c-2 0-3.6-0.6-4.7-1.7c-1.1-1.1-1.6-2.7-1.6-4.6v-15.9h2.7v15.9c0 1.2 .3 2.2 .9 2.8c.6 .7 1.5 1.1 2.7 1.1c1.2 0 2.1-0.4 2.7-1.1c.6-0.7 .9-1.6 .9-2.8v-15.9h2.7v15.9c0 2-0.5 3.5-1.6 4.6c-1.1 1.1-2.6 1.7-4.7 1.7Zm17.5-0.1c-1.7 0-3.1-0.3-4.2-1.1c-1.1-0.8-1.6-1.9-1.6-3.2h2.8c0 .6 .3 1.1 .8 1.4c.6 .4 1.4 .5 2.2 .5h1.3c1.1 0 1.9-0.2 2.4-0.6c.5-0.4 .8-1 .8-1.7c0-0.7-0.3-1.3-0.8-1.7c-0.5-0.4-1.2-0.7-2.1-0.8l-2.2-0.4c-1.7-0.3-2.9-0.8-3.6-1.5c-0.8-0.8-1.2-1.9-1.2-3.2c0-1.5 .5-2.6 1.4-3.4c1-0.8 2.4-1.2 4.2-1.2h1.2c1.6 0 3 .3 4 1.1c1 .7 1.5 1.7 1.5 3h-2.7c0-0.6-0.3-1-0.8-1.3c-0.5-0.3-1.2-0.4-2.1-0.4h-1.1c-1 0-1.7 .2-2.2 .5c-0.5 .4-0.8 1-0.8 1.7c0 1.2 .9 1.9 2.6 2.2l2.2 .4c1.8 .3 3.1 .8 3.9 1.5c.8 .8 1.2 1.9 1.2 3.3c0 1.6-0.5 2.8-1.5 3.6c-0.9 .9-2.4 1.3-4.4 1.3Zm18.5 .1c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3l-0.1 2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.2c.1-1.1 .6-2 1.4-2.6c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm35.8 0v-2.5h4.5v-16.9h-4.5v-2.5h11.8l-0.1 2.5h-4.4v16.9h4.4v2.5Zm17.7 0v-16.5h2.7v3.2c.1-1.1 .6-2 1.3-2.6c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 3 .5 3.9 1.5c1 1.1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm26 0c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.1v-9.7h-4.7v-2.5h4.7v-4.7h2.7v4.7h6.6v2.5h-6.6v9.7c0 .5 .2 1 .5 1.3c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm16.2 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.7-2-0.7-3.3v-4.5c0-1.3 .2-2.4 .7-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3l-0.1 2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.2c.1-1.1 .6-2 1.4-2.6c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm21.3 0v-12.4h-4.8v-2.4h4.8v-2.9c0-1.3 .4-2.3 1.2-3.1c.9-0.7 2-1.1 3.4-1.1h4.9v2.5h-4.9c-1.3 0-1.9 .5-1.9 1.7v2.9h6.8v2.4h-6.8v12.4Zm19 .3c-1.8 0-3.1-0.4-4.1-1.3c-1-0.9-1.5-2.1-1.5-3.6c0-1.6 .5-2.8 1.5-3.7c1-0.9 2.3-1.3 4-1.3h5.1v-1.6c0-1-0.3-1.8-0.9-2.4c-0.6-0.5-1.5-0.8-2.5-0.8c-1 0-1.8 .2-2.5 .6c-0.6 .4-1 1-1.2 1.7h-2.7c.1-1 .4-1.8 1-2.5c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.2-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 1 1.6 2.3 1.6 4v11.3h-2.6v-3.1h-0.1c-0.1 1-0.6 1.9-1.5 2.5c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.4-0.3 3.2-1c.8-0.6 1.2-1.4 1.2-2.4v-2.4h-5c-0.9 0-1.6 .3-2.2 .8c-0.5 .5-0.7 1.1-0.7 2c0 .9 .3 1.6 .9 2.2c.6 .5 1.5 .8 2.6 .8Zm18.9 2.1c-2 0-3.6-0.6-4.8-1.7c-1.2-1.1-1.7-2.6-1.7-4.6v-4.5c0-2 .5-3.5 1.7-4.6c1.2-1.1 2.8-1.7 4.8-1.7c1.9 0 3.5 .5 4.6 1.5c1.2 1 1.8 2.4 1.8 4.2h-2.7c0-1.1-0.4-1.9-1-2.4c-0.7-0.6-1.6-0.9-2.7-0.9c-1.2 0-2.1 .3-2.8 1c-0.7 .7-1.1 1.6-1.1 2.9l.1 4.5c-0.1 1.2 .3 2.2 1 2.9c.7 .7 1.6 1 2.8 1c1.1 0 2-0.3 2.7-0.9c.6-0.6 1-1.4 1-2.4h2.7c0 1.8-0.6 3.2-1.8 4.2c-1.1 1-2.7 1.5-4.6 1.5Zm17.9 0c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.7-2-0.7-3.3v-4.5c0-1.3 .2-2.4 .7-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .8 2 .8 3.3v2.9h-10.3v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1.1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1-2.8-1c-1.2 0-2.2 .3-2.8 1c-0.7 .7-1 1.7-1 3Zm21.3 9.8c-1.7 0-3.1-0.3-4.2-1.1c-1.1-0.8-1.6-1.9-1.6-3.2h2.8c0 .6 .3 1.1 .8 1.4c.6 .4 1.4 .5 2.3 .5h1.2c1.1 0 1.9-0.2 2.4-0.6c.5-0.4 .8-1 .8-1.7c0-0.7-0.3-1.3-0.8-1.7c-0.5-0.4-1.2-0.7-2.1-0.8l-2.2-0.4c-1.6-0.3-2.9-0.8-3.6-1.5c-0.8-0.8-1.2-1.9-1.2-3.2c0-1.5 .5-2.6 1.4-3.4c1-0.8 2.4-1.2 4.2-1.2h1.2c1.7 0 3 .3 4 1.1c1 .7 1.5 1.7 1.5 3h-2.7c0-0.6-0.3-1-0.8-1.3c-0.5-0.3-1.2-0.4-2.1-0.4h-1.1c-1 0-1.7 .2-2.2 .5c-0.5 .4-0.8 1-0.8 1.7c0 1.2 .9 1.9 2.6 2.2l2.2 .4c1.8 .3 3.1 .8 3.9 1.5c.8 .8 1.2 1.9 1.2 3.3c0 1.6-0.5 2.8-1.5 3.6c-0.9 .9-2.4 1.3-4.3 1.3Zm38.3-0.2c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.2v-9.6h-4.6v-2.5h4.6v-4.7h2.7v4.7h6.6v2.5h-6.6v9.6c0 .6 .2 1.1 .5 1.4c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm10 0v-21.9h2.7v8.6c.1-1.2 .6-2 1.3-2.6c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 3 .5 3.9 1.5c1 1.1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.7-1.5-1-2.6-1c-1.1 0-2 .3-2.7 1c-0.6 .7-0.9 1.7-0.9 3v10.5Zm22.8 .3c-1.8 0-3.1-0.4-4.1-1.3c-1-0.9-1.5-2.1-1.5-3.7c0-1.5 .5-2.7 1.5-3.6c1-0.9 2.3-1.3 4-1.3h5.1v-1.7c0-1-0.3-1.7-0.9-2.3c-0.6-0.6-1.5-0.8-2.5-0.8c-1 0-1.8 .2-2.5 .6c-0.6 .4-1 1-1.2 1.7h-2.7c.1-1 .4-1.8 1-2.5c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.2-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 1 1.6 2.3 1.6 4v11.3h-2.6v-3.2h-0.1c-0.1 1.1-0.6 1.9-1.5 2.6c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.4-0.4 3.2-1c.8-0.6 1.2-1.5 1.2-2.5v-2.3h-5c-0.9 0-1.6 .3-2.2 .8c-0.5 .5-0.7 1.1-0.7 2c0 .9 .3 1.6 .9 2.2c.6 .5 1.5 .8 2.6 .8Zm20.6 1.8c-1.4 0-2.4-0.4-3.3-1.2c-0.8-0.8-1.2-1.8-1.2-3.2v-9.6h-4.6v-2.5h4.6v-4.7h2.7v4.7h6.6v2.5h-6.6v9.6c0 .6 .2 1.1 .5 1.4c.3 .4 .8 .5 1.3 .5h4.5v2.5Zm28.5 0v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.4-2.5c.7-0.6 1.7-0.9 3-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm23.7 .3c-1.9 0-3.4-0.6-4.6-1.7c-1.1-1.1-1.7-2.7-1.7-4.6v-10.5h2.7v10.5c0 1.2 .3 2.2 1 2.9c.6 .7 1.5 1 2.6 1c1.1 0 2-0.3 2.6-1c.7-0.7 1-1.7 1-2.9v-10.5h2.7v10.5c0 1.9-0.6 3.5-1.7 4.6c-1.2 1.1-2.7 1.7-4.6 1.7Zm11.8-0.3v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm40.8 .3c-1.8 0-3.2-0.4-4.2-1.3c-1-0.9-1.5-2.1-1.5-3.7c0-1.5 .5-2.7 1.5-3.6c1-0.9 2.4-1.3 4.1-1.3h5.1v-1.7c0-1-0.3-1.7-0.9-2.3c-0.6-0.6-1.5-0.8-2.6-0.8c-0.9 0-1.7 .2-2.4 .6c-0.6 .4-1.1 1-1.2 1.6h-2.7c.1-0.9 .4-1.7 1-2.4c.6-0.7 1.3-1.2 2.2-1.6c1-0.4 2-0.6 3.1-0.6c1.9 0 3.4 .5 4.5 1.5c1.1 .9 1.7 2.3 1.7 4v11.3h-2.6v-3.2h-0.1c-0.1 1.1-0.6 1.9-1.5 2.6c-0.9 .6-2.1 .9-3.5 .9Zm.6-2.1c1.3 0 2.3-0.4 3.2-1c.8-0.6 1.2-1.5 1.2-2.5v-2.3h-5c-0.9 0-1.7 .3-2.2 .8c-0.5 .5-0.8 1.1-0.8 2c0 .9 .4 1.6 1 2.2c.6 .5 1.5 .8 2.6 .8Zm12.6 1.8v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.6-1.5-1-2.6-1c-1.1 0-2 .4-2.7 1.1c-0.6 .6-0.9 1.6-0.9 2.9v10.5Zm20.6 5.4l2.4-6.4l-6.3-15.5h2.9l4.1 10.2c.1 .3 .2 .7 .4 1.2c.1 .5 .2 .9 .3 1.2c.1-0.3 .2-0.7 .3-1.2c.1-0.5 .3-0.9 .4-1.2l3.8-10.2h2.8l-8.3 21.9Zm16.3-5.4l-2.7-16.5h2.4l1.6 11.7c.1 .4 .2 .9 .2 1.4c.1 .5 .1 1 .1 1.3h.2c0-0.3 0-0.8 .1-1.3c.1-0.5 .1-1 .2-1.4l1.8-11.7h2.7l1.9 11.7c.1 .4 .1 .9 .2 1.4c.1 .5 .1 1 .1 1.3h.2c0-0.3 .1-0.8 .1-1.3c.1-0.5 .2-1 .2-1.4l1.7-11.7h2.2l-2.7 16.5h-3l-1.8-11.4c0-0.6-0.1-1.1-0.2-1.7c-0.1-0.5-0.1-1-0.2-1.3h-0.1c0 .3-0.1 .8-0.1 1.3c-0.1 .6-0.2 1.1-0.3 1.7l-1.8 11.4Zm17.1 0v-21.9h2.7v8.5c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.6 0 2.9 .5 3.9 1.5c1 1 1.5 2.4 1.5 4.2v11.1h-2.7v-10.8c0-1.2-0.3-2.1-0.9-2.7c-0.7-0.7-1.5-1-2.6-1c-1.1 0-2 .3-2.7 1c-0.6 .7-0.9 1.7-0.9 3v10.5Zm24.2 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .7 2 .7 3.3v2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1.1-2.8-1.1c-1.2 0-2.2 .4-2.8 1.1c-0.7 .7-1 1.7-1 3Zm16.1 9.6v-16.5h2.7v3.1c.1-1.1 .6-1.9 1.3-2.5c.8-0.6 1.8-0.9 3.1-0.9c1.7 0 3.1 .5 4.1 1.6c1 1.1 1.5 2.6 1.5 4.5v1.1h-2.7v-0.9c0-1.3-0.3-2.2-0.9-2.9c-0.7-0.7-1.6-1.1-2.8-1.1c-2.4 0-3.6 1.4-3.6 4v10.5Zm23.7 .3c-1.3 0-2.4-0.3-3.4-0.8c-1-0.5-1.7-1.2-2.3-2.2c-0.5-0.9-0.8-2-0.8-3.3v-4.5c0-1.3 .3-2.4 .8-3.3c.6-1 1.3-1.7 2.3-2.2c1-0.5 2.1-0.8 3.4-0.8c1.3 0 2.4 .3 3.4 .8c1 .5 1.7 1.2 2.3 2.2c.5 .9 .7 2 .7 3.3v2.9h-10.2v1.6c0 1.3 .3 2.3 1 3c.6 .7 1.6 1 2.8 1c1 0 1.9-0.2 2.5-0.5c.7-0.4 1-1 1.2-1.7h2.7c-0.2 1.4-0.9 2.5-2 3.3c-1.2 .8-2.7 1.2-4.4 1.2Zm-3.8-9.9h7.6v-0.9c0-1.3-0.3-2.3-1-3c-0.6-0.7-1.6-1.1-2.8-1.1c-1.2 0-2.2 .4-2.8 1.1c-0.7 .7-1 1.7-1 3Z"/><g transform="translate(764,-14.5) scale(.431111,.440249) translate(-13.9,56.6)"><path fill="#e0e0e0" stroke="#e0e0e0" stroke-linejoin="round" d="M-50.7 4h278.2c10 0 9 0 9 0l41 40c0 0 1 0-9 0h-358c-9.9 0-9 0-9 0l38.8-40c0 0-0.9 0 9 0Z" stroke-width="10" transform="translate(225,339.9) scale(0,1) translate(-89.5,-24)" style="animation: 10s linear infinite both a0_t;"/><rect width="83" height="22" stroke="#fb422d" fill="#e0e0e0" rx="2" stroke-width="0" transform="translate(225,348.9) scale(0,1) translate(-41.5,-11)" style="animation: 10s linear infinite both a1_t;"/><g style="animation: 10s linear infinite both a3_t;"><rect width="400" height="334" stroke="#e0e0e0" fill="none" stroke-width="30" stroke-miterlimit="1" rx="10" opacity="0" transform="translate(225,198.9) rotate(-90) translate(-200,-167)" style="animation: 10s linear infinite both a2_t, 10s linear infinite both a2_o, 10s linear infinite both a2_sw, 10s linear infinite both a2_w, 10s linear infinite both a2_h;"/></g><use width="168.5" height="302" xlink:href="#Symbol-2" opacity="0" transform="translate(227.8,199.9) translate(-84.2,-151)" style="animation: 10s linear infinite both a4_t, 10s linear infinite both a4_w, 10s linear infinite both a4_h;"/><rect width="16" height="8" stroke="#fb422d" fill="#e0e0e0" stroke-width="0" rx="2" opacity="0" transform="translate(225,328.9) translate(-8,-4)" style="animation: 10s linear infinite both a5_t, 10s linear infinite both a5_o, 10s linear infinite both a5_w;"/><rect width="16" height="8" stroke="#fb422d" fill="#e0e0e0" stroke-width="0" rx="2" opacity="0" transform="translate(225,328.9) translate(-8,-4)" style="animation: 10s linear infinite both a6_t, 10s linear infinite both a6_o, 10s linear infinite both a6_w;"/><g transform="translate(225,198.9) scale(1.5,1.5) translate(-42.3,-93)" style="animation: 10s linear infinite both a7_t;"><g transform="translate(42.3,93) scale(5.352,6.9) translate(-16,60)"><g transform="translate(-34.9,-125.8)"><g transform="translate(24,5)"><path d="M33 46.5c0 3.1-0.8 5.5-2.2 7.4c-1.4 1.9-3.3 3.6-5.4 5.4c-2.2 1.7-4.5 3.6-6.3 6.2c-1.8 2.5-3.1 5.5-3.1 9.5h4.7c0-3.1 .9-5.4 2.2-7.3c1.4-1.9 3.3-3.5 5.5-5.3c2.1-1.8 4.4-3.7 6.2-6.2c1.8-2.6 3.1-5.6 3.1-9.7Z" fill="#e96020"/><path d="M20.4 70.7c-0.6 0-1 .5-1 1.1c0 .7 .4 1.2 1 1.2h12.9c.7 0 1.2-0.5 1.2-1.2c0-0.6-0.5-1.1-1.2-1.1Z" fill="#2d323b"/><path d="M21.8 66.6c-0.7 0-1 .6-1 1.2c0 .6 .3 1 .9 1h10.3c.6 0 1.1-0.5 1.1-1.1c0-0.6-0.5-1.1-1.1-1.1Z" fill="#2d323b"/><path d="M21.8 53c-0.7 0-1.2 .5-1.1 1.1c0 .6 .4 1.1 1.1 1.1h10.2c.6 0 .9-0.6 .9-1.2c0-0.6-0.3-1-0.9-1Z" fill="#2d323b"/><path d="M20.4 48.8c-0.6 0-1.2 .5-1.2 1.1c0 .7 .6 1.2 1.2 1.2h12.9c.7 0 1-0.5 1-1.1c0-0.7-0.3-1.2-1-1.2Z" fill="#2d323b"/><path d="M16 46.5c0 4.1 1.3 7.1 3.1 9.7c1.8 2.5 4.1 4.4 6.3 6.2c2.1 1.8 4 3.6 5.4 5.5c1.4 1.9 2.2 4 2.2 7.1h4.7c0-4-1.3-7.1-3.1-9.6c-1.8-2.5-4.1-4.4-6.2-6.2c-2.2-1.8-4.1-3.4-5.5-5.3c-1.3-1.9-2.2-4.3-2.2-7.4Z" fill="#00a8d6"/></g></g></g></g></g></svg>
···
+130
crates/weaver-server/assets/styling/entry-card.css
···
··· 1 + /* Entry card styling */ 2 + 3 + .entries-list { 4 + max-width: 800px; 5 + margin: 0 auto; 6 + padding: 2rem 1rem; 7 + } 8 + 9 + .entry-card { 10 + margin-bottom: calc(1.25rem * var(--spacing-scale, 1.25)); 11 + } 12 + 13 + .entry-card-link { 14 + display: block; 15 + background: var(--color-surface); 16 + border: 1px solid var(--color-border); 17 + border-radius: 6px; 18 + padding: 1.25rem; 19 + text-decoration: none; 20 + color: var(--color-text); 21 + transition: all 0.2s ease; 22 + } 23 + 24 + .entry-card-link:hover { 25 + background: var(--color-overlay); 26 + border-color: var(--color-secondary); 27 + box-shadow: 0 2px 6px color-mix(in srgb, var(--color-secondary) 8%, transparent); 28 + transform: translateY(-1px); 29 + } 30 + 31 + .entry-card-header { 32 + } 33 + 34 + .entry-card-title { 35 + margin-left: auto; 36 + font-size: 1.25rem; 37 + font-weight: 600; 38 + color: var(--color-primary); 39 + margin: 0; 40 + font-family: var(--font-heading); 41 + } 42 + 43 + .entry-card-meta { 44 + display: flex; 45 + align-items: center; 46 + gap: 1rem; 47 + margin-bottom: 0.5rem; 48 + font-size: 0.875rem; 49 + color: var(--color-subtle); 50 + } 51 + 52 + .entry-card-author { 53 + margin-left: auto; 54 + display: flex; 55 + align-items: center; 56 + gap: 0.5rem; 57 + } 58 + 59 + .entry-card-author .author-name { 60 + font-weight: 500; 61 + color: var(--color-text); 62 + } 63 + 64 + .entry-card-date { 65 + color: var(--color-muted); 66 + font-size: 0.8rem; 67 + } 68 + 69 + .entry-card-tags { 70 + display: flex; 71 + gap: 0.4rem; 72 + flex-wrap: wrap; 73 + } 74 + 75 + .entry-card-tag { 76 + padding: 0.2rem 0.5rem; 77 + background: var(--color-base); 78 + border: 1px solid var(--color-border); 79 + border-radius: 3px; 80 + font-size: 0.75rem; 81 + color: var(--color-subtle); 82 + transition: all 0.15s ease; 83 + } 84 + 85 + .entry-card-link:hover .entry-card-tag { 86 + background: var(--color-surface); 87 + border-color: var(--color-tertiary); 88 + } 89 + 90 + .loading, 91 + .error { 92 + text-align: center; 93 + padding: 2rem; 94 + color: var(--color-muted); 95 + } 96 + 97 + .error { 98 + color: var(--color-error); 99 + } 100 + 101 + .entry-card-preview { 102 + margin-top: 0.5rem; 103 + color: var(--color-subtle); 104 + font-size: 0.875rem; 105 + line-height: 1.5; 106 + display: -webkit-box; 107 + -webkit-line-clamp: 4; 108 + -webkit-box-orient: vertical; 109 + overflow: hidden; 110 + } 111 + 112 + .entry-card-preview h1, 113 + .entry-card-preview h2, 114 + .entry-card-preview h3, 115 + .entry-card-preview h4, 116 + .entry-card-preview h5, 117 + .entry-card-preview h6 { 118 + font-size: 0.875rem; 119 + font-weight: 600; 120 + margin: 0.5rem 0; 121 + } 122 + 123 + .entry-card-preview p { 124 + margin: 0; 125 + display: inline; 126 + } 127 + 128 + .entry-card-preview code { 129 + font-size: 0.8rem; 130 + }
+4
crates/weaver-server/assets/styling/entry.css
··· 181 background: var(--color-surface); 182 } 183 184 .entry-content-main blockquote { 185 border-left-color: var(--color-secondary); 186 background: var(--color-surface);
··· 181 background: var(--color-surface); 182 } 183 184 + .entry-content-main pre code { 185 + background: var(--color-surface); 186 + } 187 + 188 .entry-content-main blockquote { 189 border-left-color: var(--color-secondary); 190 background: var(--color-surface);
+108
crates/weaver-server/assets/styling/notebook-card.css
···
··· 1 + /* Notebook card styling */ 2 + 3 + .notebooks-list { 4 + max-width: 800px; 5 + margin: 0 auto; 6 + padding: 2rem 1rem; 7 + } 8 + 9 + .notebook-card { 10 + margin-bottom: calc(1.5rem * var(--spacing-scale, 1.25)); 11 + } 12 + 13 + .notebook-card-link { 14 + display: block; 15 + background: var(--color-surface); 16 + border: 1px solid var(--color-border); 17 + border-radius: 8px; 18 + padding: 1.5rem; 19 + text-decoration: none; 20 + color: var(--color-text); 21 + transition: all 0.2s ease; 22 + } 23 + 24 + .notebook-card-link:hover { 25 + background: var(--color-overlay); 26 + border-color: var(--color-primary); 27 + } 28 + 29 + .notebook-card-header { 30 + margin-bottom: 1rem; 31 + } 32 + 33 + .notebook-card-title { 34 + font-size: 1.5rem; 35 + font-weight: 600; 36 + color: var(--color-primary); 37 + margin: 0 0 0.5rem 0; 38 + font-family: var(--font-heading); 39 + } 40 + 41 + .notebook-card-description { 42 + color: var(--color-muted); 43 + line-height: 1.5; 44 + margin: 0; 45 + display: -webkit-box; 46 + -webkit-line-clamp: 2; 47 + -webkit-box-orient: vertical; 48 + overflow: hidden; 49 + } 50 + 51 + .notebook-card-meta { 52 + display: flex; 53 + align-items: center; 54 + gap: 1rem; 55 + flex-wrap: wrap; 56 + margin-bottom: 0.75rem; 57 + font-size: 0.9rem; 58 + color: var(--color-subtle); 59 + } 60 + 61 + .notebook-card-author { 62 + display: flex; 63 + align-items: center; 64 + gap: 0.5rem; 65 + } 66 + 67 + .notebook-card-author .author-name { 68 + font-weight: 500; 69 + color: var(--color-text); 70 + } 71 + 72 + .notebook-card-date { 73 + margin-left: auto; 74 + color: var(--color-muted); 75 + font-size: 0.85rem; 76 + } 77 + 78 + .notebook-card-tags { 79 + display: flex; 80 + gap: 0.5rem; 81 + flex-wrap: wrap; 82 + } 83 + 84 + .notebook-card-tag { 85 + padding: 0.25rem 0.625rem; 86 + background: var(--color-base); 87 + border: 1px solid var(--color-border); 88 + border-radius: 3px; 89 + font-size: 0.8rem; 90 + color: var(--color-subtle); 91 + transition: all 0.15s ease; 92 + } 93 + 94 + .notebook-card-link:hover .notebook-card-tag { 95 + background: var(--color-surface); 96 + border-color: var(--color-tertiary); 97 + } 98 + 99 + .notebook-card-preview { 100 + margin-top: 0.75rem; 101 + color: var(--color-subtle); 102 + font-size: 0.9rem; 103 + line-height: 1.5; 104 + display: -webkit-box; 105 + -webkit-line-clamp: 3; 106 + -webkit-box-orient: vertical; 107 + overflow: hidden; 108 + }
crates/weaver-server/assets/weaver_photo_sm.jpg

This is a binary file and will not be displayed.

+126 -13
crates/weaver-server/src/components/entry.rs
··· 6 components::avatar::{Avatar, AvatarFallback, AvatarImage}, 7 fetch, 8 }; 9 use dioxus::prelude::*; 10 11 const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); ··· 136 } 137 138 #[component] 139 - pub fn EntryCard(entry: BookEntryView<'static>) -> Element { 140 - rsx! {} 141 } 142 143 /// Metadata header showing title, authors, date, tags ··· 155 .map(|t| t.as_ref()) 156 .unwrap_or("Untitled"); 157 158 - let indexed_at_chrono = entry_view.indexed_at.as_ref(); 159 160 rsx! { 161 header { class: "entry-metadata", ··· 182 .map(|n| n.as_ref()) 183 .unwrap_or("Unknown"); 184 rsx! { 185 - if let Some(avatar) = avatar { 186 - Avatar { 187 - AvatarImage { 188 - src: avatar 189 } 190 } 191 } 192 - span { class: "author-name", "{display_name}" } 193 - span { class: "meta-label", "@{ident}" } 194 } 195 } 196 Err(_) => { ··· 220 if let Some(ref tags) = entry_view.tags { 221 div { class: "entry-tags", 222 // TODO: Parse tags structure 223 - span { class: "meta-label", "Tags: " } 224 - span { "[tags]" } 225 } 226 } 227 } ··· 237 ident: AtIdentifier<'static>, 238 book_title: SmolStr, 239 ) -> Element { 240 - use crate::Route; 241 - 242 let entry_title = entry 243 .title 244 .as_ref()
··· 6 components::avatar::{Avatar, AvatarFallback, AvatarImage}, 7 fetch, 8 }; 9 + 10 + use crate::Route; 11 use dioxus::prelude::*; 12 13 const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); ··· 138 } 139 140 #[component] 141 + pub fn EntryCard(entry: BookEntryView<'static>, book_title: SmolStr) -> Element { 142 + use crate::Route; 143 + use jacquard::{from_data, IntoStatic}; 144 + use weaver_api::app_bsky::actor::profile::Profile; 145 + use weaver_api::sh_weaver::notebook::entry::Entry; 146 + 147 + let entry_view = &entry.entry; 148 + let title = entry_view 149 + .title 150 + .as_ref() 151 + .map(|t| t.as_ref()) 152 + .unwrap_or("Untitled"); 153 + 154 + let ident = entry_view.uri.authority().clone().into_static(); 155 + let ident_for_avatar = ident.clone(); 156 + 157 + // Format date 158 + let formatted_date = entry_view 159 + .indexed_at 160 + .as_ref() 161 + .format("%B %d, %Y") 162 + .to_string(); 163 + 164 + // Get first author for display 165 + let first_author = entry_view.authors.first(); 166 + 167 + // Render preview from entry content 168 + let preview_html = from_data::<Entry>(&entry_view.record).ok().map(|entry| { 169 + let parser = markdown_weaver::Parser::new(&entry.content); 170 + let mut html_buf = String::new(); 171 + markdown_weaver::html::push_html(&mut html_buf, parser); 172 + html_buf 173 + }); 174 + 175 + rsx! { 176 + div { class: "entry-card", 177 + Link { 178 + to: Route::Entry { 179 + ident: ident.clone(), 180 + book_title: book_title.clone(), 181 + title: title.to_string().into() 182 + }, 183 + class: "entry-card-link", 184 + 185 + 186 + 187 + div { class: "entry-card-meta", 188 + div { class: "entry-card-header", 189 + 190 + h3 { class: "entry-card-title", "{title}" } 191 + div { class: "entry-card-date", 192 + time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" } 193 + } 194 + } 195 + if let Some(author) = first_author { 196 + div { class: "entry-card-author", 197 + { 198 + match from_data::<Profile>(author.record.get_at_path(".value").unwrap()) { 199 + Ok(profile) => { 200 + let avatar = profile.avatar 201 + .map(|avatar| { 202 + let cid = avatar.blob().cid(); 203 + format!("https://cdn.bsky.app/img/avatar/plain/{}/{cid}@jpeg", ident_for_avatar.as_ref()) 204 + }); 205 + let display_name = profile.display_name 206 + .as_ref() 207 + .map(|n| n.as_ref()) 208 + .unwrap_or("Unknown"); 209 + rsx! { 210 + if let Some(avatar_url) = avatar { 211 + Avatar { 212 + AvatarImage { src: avatar_url } 213 + } 214 + } 215 + span { class: "author-name", "{display_name}" } 216 + span { class: "meta-label", "@{ident}" } 217 + } 218 + } 219 + Err(_) => { 220 + rsx! { 221 + span { class: "author-name", "Author {author.index}" } 222 + } 223 + } 224 + } 225 + } 226 + } 227 + } 228 + 229 + 230 + } 231 + 232 + 233 + 234 + if let Some(ref html) = preview_html { 235 + div { class: "entry-card-preview", dangerous_inner_html: "{html}" } 236 + } 237 + if let Some(ref tags) = entry_view.tags { 238 + if !tags.is_empty() { 239 + div { class: "entry-card-tags", 240 + for tag in tags.iter() { 241 + span { class: "entry-card-tag", "{tag}" } 242 + } 243 + } 244 + } 245 + } 246 + } 247 + } 248 + } 249 } 250 251 /// Metadata header showing title, authors, date, tags ··· 263 .map(|t| t.as_ref()) 264 .unwrap_or("Untitled"); 265 266 + //let indexed_at_chrono = entry_view.indexed_at.as_ref(); 267 268 rsx! { 269 header { class: "entry-metadata", ··· 290 .map(|n| n.as_ref()) 291 .unwrap_or("Unknown"); 292 rsx! { 293 + Link { 294 + to: Route::RepositoryIndex { ident: ident.clone() }, 295 + div { class: "entry-authors", 296 + if let Some(avatar) = avatar { 297 + Avatar { 298 + AvatarImage { 299 + src: avatar 300 + } 301 + } 302 } 303 + span { class: "author-name", "{display_name}" } 304 + span { class: "meta-label", "@{ident}" } 305 } 306 } 307 } 308 } 309 Err(_) => { ··· 333 if let Some(ref tags) = entry_view.tags { 334 div { class: "entry-tags", 335 // TODO: Parse tags structure 336 + span { class: "meta-label", "Tags:" } 337 + for tag in tags.iter() { 338 + span { class: "meta-label", "{tag}" } 339 + } 340 } 341 } 342 } ··· 352 ident: AtIdentifier<'static>, 353 book_title: SmolStr, 354 ) -> Element { 355 let entry_title = entry 356 .title 357 .as_ref()
+98 -8
crates/weaver-server/src/components/identity.rs
··· 3 use jacquard::types::ident::AtIdentifier; 4 use weaver_api::sh_weaver::notebook::NotebookView; 5 6 #[component] 7 pub fn Repository(ident: AtIdentifier<'static>) -> Element { 8 rsx! { ··· 18 let fetcher = use_context::<fetch::CachedFetcher>(); 19 let notebooks = use_signal(|| fetcher.list_recent_notebooks()); 20 rsx! { 21 - for notebook in notebooks.iter() { 22 - { 23 - let view = &notebook.0; 24 - rsx! { 25 - div { 26 - key: "{view.cid}", 27 - NotebookCard { notebook: view.clone() } 28 } 29 } 30 } ··· 34 35 #[component] 36 pub fn NotebookCard(notebook: NotebookView<'static>) -> Element { 37 - rsx! {} 38 }
··· 3 use jacquard::types::ident::AtIdentifier; 4 use weaver_api::sh_weaver::notebook::NotebookView; 5 6 + const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 7 + 8 #[component] 9 pub fn Repository(ident: AtIdentifier<'static>) -> Element { 10 rsx! { ··· 20 let fetcher = use_context::<fetch::CachedFetcher>(); 21 let notebooks = use_signal(|| fetcher.list_recent_notebooks()); 22 rsx! { 23 + document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } 24 + 25 + div { class: "notebooks-list", 26 + for notebook in notebooks.iter() { 27 + { 28 + let view = &notebook.0; 29 + rsx! { 30 + div { 31 + key: "{view.cid}", 32 + NotebookCard { notebook: view.clone() } 33 + } 34 } 35 } 36 } ··· 40 41 #[component] 42 pub fn NotebookCard(notebook: NotebookView<'static>) -> Element { 43 + use crate::components::avatar::{Avatar, AvatarImage}; 44 + use jacquard::{from_data, prelude::IdentityResolver, IntoStatic}; 45 + use weaver_api::app_bsky::actor::profile::Profile; 46 + use weaver_api::sh_weaver::notebook::book::Book; 47 + 48 + let title = notebook 49 + .title 50 + .as_ref() 51 + .map(|t| t.as_ref()) 52 + .unwrap_or("Untitled Notebook"); 53 + 54 + // Format date 55 + let formatted_date = notebook.indexed_at.as_ref().format("%B %d, %Y").to_string(); 56 + 57 + // Get first author for display 58 + let first_author = notebook.authors.first(); 59 + 60 + let ident = notebook.uri.authority().clone().into_static(); 61 + let ident_for_avatar = ident.clone(); 62 + 63 + rsx! { 64 + div { class: "notebook-card", 65 + Link { 66 + to: Route::Entry { 67 + ident, 68 + book_title: title.to_string().into(), 69 + title: "".into() // Will redirect to first entry 70 + }, 71 + class: "notebook-card-link", 72 + 73 + div { class: "notebook-card-header", 74 + h2 { class: "notebook-card-title", "{title}" } 75 + } 76 + 77 + div { class: "notebook-card-meta", 78 + if let Some(author) = first_author { 79 + div { class: "notebook-card-author", 80 + { 81 + match from_data::<Profile>(author.record.get_at_path(".value").unwrap()) { 82 + Ok(profile) => { 83 + let avatar = profile.avatar 84 + .map(|avatar| { 85 + let cid = avatar.blob().cid(); 86 + format!("https://cdn.bsky.app/img/avatar/plain/{}/{cid}@jpeg", ident_for_avatar.as_ref()) 87 + }); 88 + let display_name = profile.display_name 89 + .as_ref() 90 + .map(|n| n.as_ref()) 91 + .unwrap_or("Unknown"); 92 + rsx! { 93 + if let Some(avatar_url) = avatar { 94 + Avatar { 95 + AvatarImage { src: avatar_url } 96 + } 97 + } 98 + span { class: "author-name", "{display_name}" } 99 + } 100 + } 101 + Err(_) => { 102 + rsx! { 103 + span { class: "author-name", "Author {author.index}" } 104 + } 105 + } 106 + } 107 + } 108 + } 109 + } 110 + 111 + div { class: "notebook-card-date", 112 + time { datetime: "{notebook.indexed_at.as_str()}", "{formatted_date}" } 113 + } 114 + } 115 + 116 + if let Some(ref tags) = notebook.tags { 117 + if !tags.is_empty() { 118 + div { class: "notebook-card-tags", 119 + for tag in tags.iter() { 120 + span { class: "notebook-card-tag", "{tag}" } 121 + } 122 + } 123 + } 124 + } 125 + } 126 + } 127 + } 128 }
+2 -2
crates/weaver-server/src/components/mod.rs
··· 8 mod entry; 9 pub use entry::{Entry, EntryCard}; 10 11 - mod identity; 12 - pub use identity::{Repository, RepositoryIndex}; 13 pub mod avatar;
··· 8 mod entry; 9 pub use entry::{Entry, EntryCard}; 10 11 + pub mod identity; 12 + pub use identity::{NotebookCard, Repository, RepositoryIndex}; 13 pub mod avatar;
+24
crates/weaver-server/src/fetch.rs
··· 93 ) -> Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 94 cache_impl::iter(&self.book_cache) 95 } 96 }
··· 93 ) -> Vec<Arc<(NotebookView<'static>, Vec<StrongRef<'static>>)>> { 94 cache_impl::iter(&self.book_cache) 95 } 96 + 97 + pub async fn list_notebook_entries( 98 + &self, 99 + ident: AtIdentifier<'static>, 100 + book_title: SmolStr, 101 + ) -> Result<Option<Vec<BookEntryView<'static>>>> { 102 + use weaver_common::view::view_entry; 103 + 104 + if let Some(result) = self.get_notebook(ident.clone(), book_title).await? { 105 + let (notebook, entries) = result.as_ref(); 106 + let mut book_entries = Vec::new(); 107 + 108 + for index in 0..entries.len() { 109 + match view_entry(self.client.clone(), notebook, entries, index).await { 110 + Ok(book_entry) => book_entries.push(book_entry), 111 + Err(_) => continue, // Skip entries that fail to load 112 + } 113 + } 114 + 115 + Ok(Some(book_entries)) 116 + } else { 117 + Ok(None) 118 + } 119 + } 120 }
+1 -7
crates/weaver-server/src/main.rs
··· 58 59 // We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root. 60 // The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles. 61 - const FAVICON: Asset = asset!("/assets/favicon.ico"); 62 // The asset macro also minifies some assets like CSS and JS to make bundled smaller 63 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 64 ··· 119 // The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside. 120 use_context_provider(|| fetch::CachedFetcher::new(Arc::new(BasicClient::unauthenticated()))); 121 rsx! { 122 - // In addition to element and text (which we will see later), rsx can contain other components. In this case, 123 - // we are using the `document::Link` component to add a link to our favicon and main CSS file into the head of our app. 124 document::Link { rel: "icon", href: FAVICON } 125 document::Link { rel: "stylesheet", href: MAIN_CSS } 126 - 127 - 128 - // The router component renders the route enum we defined above. It will handle synchronization of the URL and render 129 - // the layouts and components for the active route. 130 Router::<Route> {} 131 } 132 }
··· 58 59 // We can import assets in dioxus with the `asset!` macro. This macro takes a path to an asset relative to the crate root. 60 // The macro returns an `Asset` type that will display as the path to the asset in the browser or a local path in desktop bundles. 61 + const FAVICON: Asset = asset!("/assets/weaver_photo_sm.jpg"); 62 // The asset macro also minifies some assets like CSS and JS to make bundled smaller 63 const MAIN_CSS: Asset = asset!("/assets/styling/main.css"); 64 ··· 119 // The `rsx!` macro lets us define HTML inside of rust. It expands to an Element with all of our HTML inside. 120 use_context_provider(|| fetch::CachedFetcher::new(Arc::new(BasicClient::unauthenticated()))); 121 rsx! { 122 document::Link { rel: "icon", href: FAVICON } 123 document::Link { rel: "stylesheet", href: MAIN_CSS } 124 Router::<Route> {} 125 } 126 }
+15 -9
crates/weaver-server/src/views/home.rs
··· 1 - use crate::{components::EntryCard, fetch}; 2 use dioxus::prelude::*; 3 4 /// The Home page component that will be rendered when the current route is `[Route::Home]` 5 #[component] 6 pub fn Home() -> Element { 7 let fetcher = use_context::<fetch::CachedFetcher>(); 8 - let entries = use_signal(|| fetcher.list_recent_entries()); 9 rsx! { 10 - for entry in entries.iter() { 11 - { 12 - let view = &entry.0; 13 - rsx! { 14 - div { 15 - key: "{view.entry.cid}", 16 - EntryCard { entry: view.clone() } 17 } 18 } 19 }
··· 1 + use crate::{components::identity::NotebookCard, fetch}; 2 use dioxus::prelude::*; 3 + 4 + const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 5 6 /// The Home page component that will be rendered when the current route is `[Route::Home]` 7 #[component] 8 pub fn Home() -> Element { 9 let fetcher = use_context::<fetch::CachedFetcher>(); 10 + let notebooks = use_signal(|| fetcher.list_recent_notebooks()); 11 rsx! { 12 + document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } 13 + 14 + div { class: "notebooks-list", 15 + for notebook in notebooks.iter() { 16 + { 17 + let view = &notebook.0; 18 + rsx! { 19 + div { 20 + key: "{view.cid}", 21 + NotebookCard { notebook: view.clone() } 22 + } 23 } 24 } 25 }
+31 -3
crates/weaver-server/src/views/notebook.rs
··· 1 - use crate::{components::NotebookCss, fetch, Route}; 2 use dioxus::prelude::*; 3 use jacquard::{ 4 smol_str::{SmolStr, ToSmolStr}, 5 types::ident::AtIdentifier, 6 }; 7 8 /// The Blog page component that will be rendered when the current route is `[Route::Blog]` 9 /// ··· 19 20 #[component] 21 pub fn NotebookIndex(ident: AtIdentifier<'static>, book_title: SmolStr) -> Element { 22 - let _fetcher = use_context::<fetch::CachedFetcher>(); 23 - rsx! {} 24 }
··· 1 + use crate::{ 2 + components::{EntryCard, NotebookCss}, 3 + fetch, Route, 4 + }; 5 use dioxus::prelude::*; 6 use jacquard::{ 7 smol_str::{SmolStr, ToSmolStr}, 8 types::ident::AtIdentifier, 9 }; 10 + 11 + const ENTRY_CARD_CSS: Asset = asset!("/assets/styling/entry-card.css"); 12 13 /// The Blog page component that will be rendered when the current route is `[Route::Blog]` 14 /// ··· 24 25 #[component] 26 pub fn NotebookIndex(ident: AtIdentifier<'static>, book_title: SmolStr) -> Element { 27 + let fetcher = use_context::<fetch::CachedFetcher>(); 28 + let book_title_clone = book_title.clone(); 29 + 30 + let notebook_entries = use_resource(use_reactive!(|(ident, book_title)| { 31 + let fetcher = fetcher.clone(); 32 + async move { 33 + fetcher.list_notebook_entries(ident, book_title).await.ok().flatten() 34 + } 35 + })); 36 + 37 + rsx! { 38 + document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } 39 + 40 + div { class: "entries-list", 41 + match &*notebook_entries.read_unchecked() { 42 + Some(Some(entries)) => rsx! { 43 + for entry in entries { 44 + EntryCard { entry: entry.clone(), book_title: book_title_clone.clone() } 45 + } 46 + }, 47 + Some(None) => rsx! { div { class: "error", "Notebook not found" } }, 48 + None => rsx! { div { class: "loading", "Loading notebook..." } } 49 + } 50 + } 51 + } 52 }
+1 -1
docs/index.html
··· 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8" /> 5 - <link rel="icon" href="./weaver_photo.jpg" /> 6 <title>Weaver</title> 7 </head> 8 <body>
··· 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8" /> 5 + <link rel="icon" href="./weaver_photo_sm.jpg" /> 6 <title>Weaver</title> 7 </head> 8 <body>
docs/weaver_photo_med.jpg

This is a binary file and will not be displayed.

docs/weaver_photo_sm.jpg

This is a binary file and will not be displayed.

+3 -1
weaver_notes/.obsidian/app.json
··· 1 - {}
··· 1 + { 2 + "alwaysUpdateLinks": true 3 + }
+14 -11
weaver_notes/.obsidian/workspace.json
··· 13 "state": { 14 "type": "markdown", 15 "state": { 16 - "file": "Long-form writing.md", 17 "mode": "source", 18 "source": false 19 }, 20 "icon": "lucide-file", 21 - "title": "Long-form writing" 22 } 23 } 24 ] ··· 94 "state": { 95 "type": "backlink", 96 "state": { 97 - "file": "Long-form writing.md", 98 "collapseAll": false, 99 "extraContext": false, 100 "sortOrder": "alphabetical", ··· 104 "unlinkedCollapsed": true 105 }, 106 "icon": "links-coming-in", 107 - "title": "Backlinks for Long-form writing" 108 } 109 }, 110 { ··· 113 "state": { 114 "type": "outgoing-link", 115 "state": { 116 - "file": "Long-form writing.md", 117 "linksCollapsed": false, 118 "unlinkedCollapsed": true 119 }, 120 "icon": "links-going-out", 121 - "title": "Outgoing links from Long-form writing" 122 } 123 }, 124 { ··· 142 "state": { 143 "type": "outline", 144 "state": { 145 - "file": "Long-form writing.md", 146 "followCursor": false, 147 "showSearch": false, 148 "searchQuery": "" 149 }, 150 "icon": "lucide-list", 151 - "title": "Outline of Long-form writing" 152 } 153 } 154 ] 155 } 156 ], 157 "direction": "horizontal", 158 - "width": 300, 159 - "collapsed": true 160 }, 161 "left-ribbon": { 162 "hiddenItems": { ··· 171 }, 172 "active": "f41bdf1f327c8668", 173 "lastOpenFiles": [ 174 - "Long-form writing.md" 175 ] 176 }
··· 13 "state": { 14 "type": "markdown", 15 "state": { 16 + "file": "Weaver - Long-form writing.md", 17 "mode": "source", 18 "source": false 19 }, 20 "icon": "lucide-file", 21 + "title": "Weaver - Long-form writing" 22 } 23 } 24 ] ··· 94 "state": { 95 "type": "backlink", 96 "state": { 97 + "file": "Weaver - Long-form writing.md", 98 "collapseAll": false, 99 "extraContext": false, 100 "sortOrder": "alphabetical", ··· 104 "unlinkedCollapsed": true 105 }, 106 "icon": "links-coming-in", 107 + "title": "Backlinks for Weaver - Long-form writing" 108 } 109 }, 110 { ··· 113 "state": { 114 "type": "outgoing-link", 115 "state": { 116 + "file": "Weaver - Long-form writing.md", 117 "linksCollapsed": false, 118 "unlinkedCollapsed": true 119 }, 120 "icon": "links-going-out", 121 + "title": "Outgoing links from Weaver - Long-form writing" 122 } 123 }, 124 { ··· 142 "state": { 143 "type": "outline", 144 "state": { 145 + "file": "Weaver - Long-form writing.md", 146 "followCursor": false, 147 "showSearch": false, 148 "searchQuery": "" 149 }, 150 "icon": "lucide-list", 151 + "title": "Outline of Weaver - Long-form writing" 152 } 153 } 154 ] 155 } 156 ], 157 "direction": "horizontal", 158 + "width": 300 159 }, 160 "left-ribbon": { 161 "hiddenItems": { ··· 170 }, 171 "active": "f41bdf1f327c8668", 172 "lastOpenFiles": [ 173 + "weaver_photo_med.jpg", 174 + "xkcd_345_excerpt.png", 175 + "Weaver - Long-form writing.md", 176 + "light_mode_excerpt.png", 177 + "notebook_entry_preview.png" 178 ] 179 }
-12
weaver_notes/Long-form writing.md
··· 1 - 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. I was super into reddit for a long time. Big fan of Fanfiction.net and later Archive of Our Own. 2 - 3 - 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. Because while I wasn't huge into independent internet forums, the broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there. I am an atheist in large part because of a blog called [Common Sense Atheism](http://commonsenseatheism.com) (which I started reading in part because the author, Luke Muehlhauser, was criticising both Richard Dawkins and some Christian apologetics I was familiar with). Luke's blog was part of cluster of blogs out of which grew the [rationalists](https://www.lesswrong.com/), one of, for better or for worse, the most influential intellectual movements of the 21st century, who are, via people like [Scott ](https://slatestarcodex.com/) [Alexander](https://www.astralcodexten.com/), both downstream and upstream of the tech billionaire ideology. I also read blogs like [boingboing.net](https://boingboing.net/), was a big fan of Cory Doctorow. I figured out I am trans in part because of [Thing of Things](https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/), 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. Amusingly I now think that being on Twitter and now Bluesky made me a better writer. [Restrictions breed creativity](https://articles.starcitygames.com/articles/restrictions-breed-creativity/), after all. 4 - 5 - But through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress, even their hosted version, required a lot of setup to really be functional, Tumblr's system for comment/replies was and remains insane, hosting my own seemed like too much money to burn on something nobody might even read at the time, and honestly I felt like I kinda missed the boat on discoverability, as the internet grew larger and more centralised, with platforms like Substack eating what was left of the blogosphere. But at the same time, its 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, and which don't make sense on a topic-based forum, or a place like Archive of our Own. Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists. 6 - 7 - That's where the `at://` protocol and Weaver comes in. 8 - ### The pitch 9 - Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto. 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, to ideate, to document, and to inform. The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files, such as an Obsidian vault or git repository documentation, into a static "notebook" site. The file is uploaded to your PDS, where it can be accessed, either directly, via a minimal app server layer that provides a friendlier web address than an XRPC request or CDN link, or hosted on a platform of your choice, be that your own server or any other static site hosting service. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app. The ultimate goal is to 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 AT protocol. 10 - 11 - ### So what about... 12 - When I started working on Weaver back in the spring, the only real games in town for long-form blogging based on atproto, aside from rolling your own, [piss.beauty](https://piss.beauty) style, were [whtwnd.com](https://whtwnd.com/) and [leaflet.pub](https://leaflet.pub/home). Leaflet's good, and it's gotten a lot better in the last year, but it doesn't offer quite what I'm looking for either. For one, I am a Markdown fangirl, for better or for worse, and while Leaflet allows you to use Markdown for formatting, it doesn't speak it natively. There are [more alternatives now](https://connectedplaces.leaflet.pub/3m4qgpc7h3223), which makes sense as this space definitely feels like one that has gaps to fill. And the `at://` protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macro"blogging, too. The interoperability the protocol allows is incredible. Weaver's app server can display Whitewind posts very easily. With some effort on my part, it can faithfully render Leaflet posts. It doesn't care what app your profile is on, it uses the [partial understanding](https://sdr-podcast.com/episodes/partial-understanding/) [capabilities](https://bsky.app/profile/nonbinary.computer/post/3m44ooo42xc2j) of the [jacquard](https://tangled.org/@nonbinary.computer/jacquard/) library I created to pull useful data out of it.
···
+36
weaver_notes/Weaver - Long-form writing.md
···
··· 1 + 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. I was super into reddit for a long time. Big fan of Fanfiction.net and later Archive of Our Own. 2 + 3 + > ![[weaver_photo_med.jpg]]*The namesake of what I'm building* 4 + 5 + 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. Because while I wasn't huge into independent internet forums, the broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there. I am an atheist in large part because of a blog called [Common Sense Atheism](http://commonsenseatheism.com) (which I started reading in part because the author, Luke Muehlhauser, was criticising both Richard Dawkins and some Christian apologetics I was familiar with). Luke's blog was part of cluster of blogs out of which grew the [rationalists](https://www.lesswrong.com/), one of, for better or for worse, the most influential intellectual movements of the 21st century, who are, via people like [Scott ](https://slatestarcodex.com/) [Alexander](https://www.astralcodexten.com/), both downstream and upstream of the tech billionaire ideology. I also read blogs like [boingboing.net](https://boingboing.net/), was a big fan of Cory Doctorow. I figured out I am trans in part because of [Thing of Things](https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/), 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. Amusingly I now think that being on Twitter and now Bluesky made me a better writer. [Restrictions breed creativity](https://articles.starcitygames.com/articles/restrictions-breed-creativity/), after all. 6 + 7 + >![[xkcd_345_excerpt.png]] 8 + >https://xkcd.com/345 *2000s internet culture was weird* 9 + 10 + But through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress, even their hosted version, required a lot of setup to really be functional, Tumblr's system for comment/replies was and remains insane, hosting my own seemed like too much money to burn on something nobody might even read at the time, and honestly I felt like I kinda missed the boat on discoverability, as the internet grew larger and more centralised, with platforms like Substack eating what was left of the blogosphere. But at the same time, its 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, and which don't make sense on a topic-based forum, or a place like Archive of our Own. Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists. 11 + 12 + That's where the `at://` protocol and Weaver comes in. 13 + ### The pitch 14 + Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto. 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, to ideate, to document, and to inform. The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files, such as an Obsidian vault or git repository documentation, into a static "notebook" site. The file is uploaded to your PDS, where it can be accessed, either directly, via a minimal app server layer that provides a friendlier web address than an XRPC request or CDN link, or hosted on a platform of your choice, be that your own server or any other static site hosting service. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app. The ultimate goal is to 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 `at://` protocol. 15 + 16 + ![[notebook_entry_preview.png]] 17 + ## Weaver 18 + Weaver works on a concept of notebooks with entries, which can be grouped into pages or chapters. They can potentially have multiple attributed authors. You can tear out a metaphorical page or entry or chapter and stick it in another notebook. Technically you can do this with entries you don't control (i.e. entries in notebooks where you're not listed as an author), although this isn't a supported mode. You own what you write. And once collaborative editing is in, collaborative work will be resilient against deletion by one author, to some degree. They can delete their notebook or even their account, but what you write will be safe, and anything you've touched, edited, will be recoverable. 19 + 20 + Entries are Markdown text. Specifically, they're an extension on the Obsidian flavour of Markdown, so they support additional embed types, including atproto record embeds and other markdown documents, as well as a two-column mode and resizable images. They will support Bluesky-based comments, but a Weaver-native system may come into the mix down the line. Currently you have to write notebook entries in an external editor and upload them. Ultimately there will be a web-based editor with live collaborative editing for those who prefer the WYSIWYG experience, intend to collaborate, or want an all-in-one option. 21 + ### So what about... 22 + When I started working on Weaver back in the spring, the only real games in town for long-form blogging based on atproto, aside from rolling your own, [piss.beauty](https://piss.beauty) style, were [whtwnd.com](https://whtwnd.com/) and [leaflet.pub](https://leaflet.pub/home). Leaflet's good, and it's gotten a lot better in the last year, but it doesn't offer quite what I'm looking for either. For one, I am a Markdown fangirl, for better or for worse, I love being able to compose stuff in a random text editor, paste it somewhere, and get a reasonably formatted, presentable, even pretty document out of it. And while Leaflet allows you to use Markdown for formatting, it doesn't speak it natively. Whitewind... There are [more alternatives now](https://connectedplaces.leaflet.pub/3m4qgpc7h3223), which makes sense as this space definitely feels like one that has gaps to fill. And the `at://` protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macro"blogging, too. The interoperability the protocol allows is incredible. Weaver's app server can display Whitewind posts very easily. With some effort on my part, it can faithfully render Leaflet posts. It doesn't care what app your profile is on, it uses the [partial understanding](https://sdr-podcast.com/episodes/partial-understanding/) [capabilities](https://bsky.app/profile/nonbinary.computer/post/3m44ooo42xc2j) of the [jacquard](https://tangled.org/@nonbinary.computer/jacquard/) library I created to pull useful data out of it. 23 + 24 + ![[light_mode_excerpt.png]] 25 + ## Where are we now? 26 + The current state of Weaver is that proof-of-concept, described in the pitch. There's a command-line tool which can either parse a single Markdown file or a whole folder of them, either rendering them to HTML into a local directory, suitable for static site hosting, or, after doing some pre-processing, uploading them and any associated media to your PDS. 27 + 28 + > The static site path is largely independent of atproto. I wrote it both as sort of the ultimate fallback, and also because I wanted something to use to write docs that go up on GitHub Pages or similar, to turn an Obsidian vault full of documentation into a website without having to pay them a monthly hosting fee. The original v0.0.1 version of this I wrote a couple of years ago, though the current version is a complete rewrite. I forked the popular rust markdown processing library [`pulldown-cmark`](https://pulldown-cmark.github.io/pulldown-cmark/) because it had limited extensibility along the axes I wanted, i.e. implementing custom syntax extensions to support Obsidian's Markdown flavour and adding some additional useful features 29 + 30 + The second half of this is the minimal app server. This is strictly a viewer for the time being and currently has no active firehose or jetstream feed and does no persistent indexing, only time-limited caching of what is requested from it. In fact, it doesn't even use constellation or slingshot (though it probably should), purely fetching records from your repository. It caches blobs and re-serves them at a known relative url path, so that the link urls are reasonable and informative, and does the same for relevant records that are linked to by notebook entries requested from it. 31 + 32 + The reason why I started with something developer-friendly rather than aiming for a non-technical audience to begin with is because I know that audience well, being one, and I figure it's far wiser to work out the kinks in the underlying implementation first with a user base capable of making a good bug report before trying for a broader audience. 33 + 34 + >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, and 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. And ultimately I may end up producing Typescript bindings for Jacquard and Weaver's core tools, if that's something people value, or I end up reconsidering doing web front-end in Rust. 35 + ### Evolution 36 + 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. If I screw this up, not too hard for someone else to pick up the torch and continue.
weaver_notes/light_mode_excerpt.png

This is a binary file and will not be displayed.

weaver_notes/notebook_entry_preview.png

This is a binary file and will not be displayed.

weaver_notes/weaver_photo_med.jpg

This is a binary file and will not be displayed.

weaver_notes/xkcd_345_excerpt.png

This is a binary file and will not be displayed.