at main 1313 lines 33 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>test-sidenotes</title> 7 <style> 8/* CSS Reset */ 9*, *::before, *::after { 10 box-sizing: border-box; 11 margin: 0; 12 padding: 0; 13} 14 15/* CSS Variables - Light Mode (default) */ 16:root { 17 --color-base: #faf4ed; 18 --color-surface: #fffaf3; 19 --color-overlay: #f2e9e1; 20 --color-text: #1f1d2e; 21 --color-muted: #635e74; 22 --color-subtle: #4a4560; 23 --color-emphasis: #1e1a2d; 24 --color-primary: #907aa9; 25 --color-secondary: #56949f; 26 --color-tertiary: #286983; 27 --color-error: #b4637a; 28 --color-warning: #ea9d34; 29 --color-success: #286983; 30 --color-border: #dfdad9; 31 --color-link: #d7827e; 32 --color-highlight: #cecacd; 33 34 --font-body: 'Adobe Caslon Pro','Latin Modern Roman','Times New Roman','serif'; 35 --font-heading: 'IBM Plex Sans','system-ui','sans-serif'; 36 --font-mono: 'Ioskeley Mono','IBM Plex Mono','Berkeley Mono','Consolas','monospace'; 37 38 --spacing-base: 16px; 39 --spacing-line-height: 1.6; 40 --spacing-scale: 1.25; 41} 42 43/* CSS Variables - Dark Mode */ 44@media (prefers-color-scheme: dark) { 45 :root { 46 --color-base: #191724; 47 --color-surface: #1f1d2e; 48 --color-overlay: #26233a; 49 --color-text: #e0def4; 50 --color-muted: #6e6a86; 51 --color-subtle: #908caa; 52 --color-emphasis: #e0def4; 53 --color-primary: #c4a7e7; 54 --color-secondary: #9ccfd8; 55 --color-tertiary: #ebbcba; 56 --color-error: #eb6f92; 57 --color-warning: #f6c177; 58 --color-success: #31748f; 59 --color-border: #403d52; 60 --color-link: #ebbcba; 61 --color-highlight: #524f67; 62 } 63} 64 65/* Base Styles */ 66html { 67 font-size: var(--spacing-base); 68 line-height: var(--spacing-line-height); 69} 70 71/* Scoped to notebook-content container */ 72.notebook-content { 73 font-family: var(--font-body); 74 color: var(--color-text); 75 background-color: var(--color-base); 76 margin: 0 auto; 77 padding: 1rem 0rem; 78 word-wrap: break-word; 79 overflow-wrap: break-word; 80 counter-reset: sidenote-counter; 81 max-width: 95ch; 82} 83 84/* When sidenotes exist, body padding creates the gutter */ 85/* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 86body:has(.sidenote) { 87 padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 88 padding-inline-end: 15.5rem; 89} 90 91/* Typography */ 92h1, h2, h3, h4, h5, h6 { 93 font-family: var(--font-heading); 94 margin-top: calc(1rem * var(--spacing-scale)); 95 margin-bottom: 0.5rem; 96 line-height: 1.2; 97} 98 99h1 { 100 font-size: 2rem; 101 color: var(--color-secondary); 102} 103h2 { 104 font-size: 1.5rem; 105 color: var(--color-primary); 106} 107h3 { 108 font-size: 1.25rem; 109 color: var(--color-secondary); 110} 111h4 { 112 font-size: 1.2rem; 113 color: var(--color-tertiary); 114} 115h5 { 116 font-size: 1.125rem; 117 color: var(--color-secondary); 118} 119h6 { font-size: 1rem; } 120 121p { 122 margin-bottom: 1rem; 123 word-wrap: break-word; 124 overflow-wrap: break-word; 125} 126 127a { 128 color: var(--color-link); 129 text-decoration: none; 130} 131 132.notebook-content a:hover { 133 color: var(--color-emphasis); 134 text-decoration: underline; 135} 136 137/* Wikilink validation (editor) */ 138.link-valid { 139 color: var(--color-link); 140} 141 142.link-broken { 143 color: var(--color-error); 144 text-decoration: underline wavy; 145 text-decoration-color: var(--color-error); 146 opacity: 0.8; 147} 148 149/* Selection */ 150::selection { 151 background: var(--color-highlight); 152 color: var(--color-text); 153} 154 155/* Lists */ 156ul, ol { 157 margin-inline-start: 1rem; 158 margin-bottom: 1rem; 159} 160 161li { 162 margin-bottom: 0.25rem; 163} 164 165/* Code */ 166code { 167 font-family: var(--font-mono); 168 background: var(--color-surface); 169 padding: 0.125rem 0.25rem; 170 border-radius: 4px; 171 font-size: 0.9em; 172} 173 174pre { 175 overflow-x: auto; 176 margin-bottom: 1rem; 177 border-radius: 5px; 178 border: 1px solid var(--color-border); 179 box-sizing: border-box; 180} 181 182/* Code blocks inside pre are handled by syntax theme */ 183pre code { 184 185 display: block; 186 width: fit-content; 187 min-width: 100%; 188 padding: 1rem; 189 background: var(--color-surface); 190} 191 192/* Math */ 193.math { 194 font-family: var(--font-mono); 195} 196 197.math-display { 198 display: block; 199 margin: 1rem 0; 200 text-align: center; 201} 202 203/* Blockquotes */ 204blockquote { 205 border-inline-start: 2px solid var(--color-secondary); 206 background: var(--color-surface); 207 padding-inline-start: 1rem; 208 padding-inline-end: 1rem; 209 padding-top: 0.5rem; 210 padding-bottom: 0.04rem; 211 margin: 1rem 0; 212 font-size: 0.95em; 213 border-bottom-right-radius: 5px; 214 border-top-right-radius: 5px; 215} 216} 217 218/* Tables */ 219table { 220 border-collapse: collapse; 221 width: 100%; 222 margin-bottom: 1rem; 223 display: block; 224 overflow-x: auto; 225 max-width: 100%; 226} 227 228th, td { 229 border: 1px solid var(--color-border); 230 padding: 0.5rem; 231 text-align: start; 232} 233 234th { 235 background: var(--color-surface); 236 font-weight: 600; 237} 238 239tr:hover { 240 background: var(--color-surface); 241} 242 243/* Footnotes */ 244.footnote-reference { 245 font-size: 0.8em; 246 color: var(--color-subtle); 247} 248 249.footnote-definition { 250 order: 9999; 251 margin: 0; 252 padding: 0.5rem 0; 253 font-size: 0.9em; 254} 255 256.footnote-definition:first-of-type { 257 margin-top: 2rem; 258 padding-top: 1rem; 259 border-top: 2px solid var(--color-border); 260} 261 262.footnote-definition:first-of-type::before { 263 content: "Footnotes"; 264 display: block; 265 font-weight: 600; 266 font-size: 1.1em; 267 color: var(--color-subtle); 268 margin-bottom: 0.75rem; 269} 270 271.footnote-definition-label { 272 font-weight: 600; 273 margin-inline-end: 0.5rem; 274 color: var(--color-primary); 275} 276 277/* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 278.notebook-content aside, 279.notebook-content .aside { 280 float: inline-start; 281 width: 40%; 282 margin: 0 1.5rem 1rem 0; 283 padding: 1rem; 284 background: var(--color-surface); 285 border-inline-end: 3px solid var(--color-primary); 286 font-size: 0.9em; 287 clear: inline-start; 288} 289 290.notebook-content aside > *:first-child, 291.notebook-content .aside > *:first-child { 292 margin-top: 0; 293} 294 295.notebook-content aside > *:last-child, 296.notebook-content .aside > *:last-child { 297 margin-bottom: 0; 298} 299 300/* Reset blockquote styling inside asides */ 301.notebook-content aside > blockquote, 302.notebook-content .aside > blockquote { 303 border-inline-start: none; 304 background: transparent; 305 padding: 0; 306 margin: 0; 307 font-size: inherit; 308} 309 310/* Indent utilities */ 311.indent-1 { margin-inline-start: 1em; } 312.indent-2 { margin-inline-start: 2em; } 313.indent-3 { margin-inline-start: 3em; } 314 315/* Tufte-style Sidenotes */ 316/* Hide checkbox for sidenote toggle */ 317.margin-toggle { 318 display: none; 319} 320 321/* Sidenote number marker (inline superscript) */ 322.sidenote-number { 323 counter-increment: sidenote-counter; 324} 325 326.sidenote-number::after { 327 content: counter(sidenote-counter); 328 font-size: 0.7em; 329 position: relative; 330 top: -0.5em; 331 color: var(--color-primary); 332 padding-inline-start: 0.1em; 333} 334 335/* Sidenote content (margin notes on wide screens) */ 336.sidenote { 337 float: inline-end; 338 clear: inline-end; 339 margin-inline-end: -15.5rem; 340 width: 14rem; 341 margin-top: 0.3rem; 342 margin-bottom: 1rem; 343 font-size: 0.85em; 344 line-height: 1.4; 345 color: var(--color-subtle); 346} 347 348.sidenote::before { 349 content: counter(sidenote-counter) ". "; 350 color: var(--color-primary); 351} 352 353/* Mobile sidenotes: toggle behavior */ 354@media (max-width: 900px) { 355 /* Reset sidenote gutter on mobile */ 356 body:has(.sidenote) { 357 padding-inline-end: 0; 358 } 359 360 aside, .aside { 361 float: none; 362 width: 100%; 363 margin: 1rem 0; 364 } 365 366 .sidenote { 367 display: none; 368 } 369 370 .margin-toggle:checked + .sidenote { 371 display: block; 372 float: none; 373 width: 95%; 374 margin: 0.5rem 2.5%; 375 padding: 0.5rem; 376 background: var(--color-surface); 377 border-inline-start: 2px solid var(--color-primary); 378 } 379 380 label.sidenote-number { 381 cursor: pointer; 382 } 383 384 label.sidenote-number::after { 385 text-decoration: underline; 386 } 387} 388 389/* Images */ 390img { 391 max-width: 100%; 392 height: auto; 393 display: block; 394 margin: 1rem 0; 395 border-radius: 4px; 396} 397 398/* Hygiene for iframes */ 399.html-embed-block { 400 max-width: 100%; 401 height: auto; 402 display: block; 403 margin: 1rem 0; 404} 405 406/* AT Protocol Embeds - Container */ 407/* Light mode: paper with shadow, dark mode: blueprint with borders */ 408.atproto-embed { 409 display: block; 410 position: relative; 411 max-width: 550px; 412 margin: 1rem 0; 413 padding: 1rem; 414 background: var(--color-surface); 415 border-inline-start: 2px solid var(--color-secondary); 416 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 417} 418 419.atproto-embed:hover { 420 border-inline-start-color: var(--color-primary); 421} 422 423@media (prefers-color-scheme: dark) { 424 .atproto-embed { 425 box-shadow: none; 426 border: 1px solid var(--color-border); 427 border-inline-start: 2px solid var(--color-secondary); 428 } 429} 430 431.atproto-embed-placeholder { 432 color: var(--color-muted); 433 font-style: italic; 434} 435 436.embed-loading { 437 display: block; 438 padding: 0.5rem 0; 439 color: var(--color-subtle); 440 font-family: var(--font-mono); 441 font-size: 0.85rem; 442} 443 444/* Embed Author Block */ 445.embed-author { 446 display: flex; 447 align-items: center; 448 gap: 0.75rem; 449 padding-bottom: 0.5rem; 450} 451 452.embed-avatar { 453 width: 36px; 454 height: 36px; 455 max-width: 36px; 456 max-height: 36px; 457 aspect-ratio: 1; 458 margin: 0; 459 object-fit: cover; 460} 461 462.embed-author-info { 463 display: flex; 464 flex-direction: column; 465 gap: 0; 466 min-width: 0; 467} 468 469.embed-avatar-link { 470 display: block; 471 flex-shrink: 0; 472} 473 474.embed-author-name { 475 font-weight: 600; 476 color: var(--color-text); 477 overflow: hidden; 478 text-overflow: ellipsis; 479 white-space: nowrap; 480 text-decoration: none; 481 line-height: 1.2; 482} 483 484a.embed-author-name:hover { 485 color: var(--color-link); 486} 487 488.embed-author-handle { 489 font-size: 0.85em; 490 font-family: var(--font-mono); 491 color: var(--color-subtle); 492 text-decoration: none; 493 overflow: hidden; 494 text-overflow: ellipsis; 495 white-space: nowrap; 496 line-height: 1.2; 497} 498 499.embed-author-handle:hover { 500 color: var(--color-link); 501} 502 503/* Card-wide clickable link (sits behind content) */ 504.embed-card-link { 505 position: absolute; 506 inset: 0; 507 z-index: 0; 508} 509 510.embed-card-link:focus { 511 outline: 2px solid var(--color-primary); 512 outline-offset: 2px; 513} 514 515/* Interactive elements sit above the card link */ 516.embed-author, 517.embed-external, 518.embed-quote, 519.embed-images, 520.embed-meta { 521 position: relative; 522 z-index: 1; 523} 524 525/* Embed Content Block */ 526.embed-content { 527 display: block; 528 color: var(--color-text); 529 line-height: 1.5; 530 margin-bottom: 0.75rem; 531 white-space: pre-wrap; 532} 533 534 535 536.embed-description { 537 display: block; 538 color: var(--color-text); 539 font-size: 0.95em; 540 line-height: 1.4; 541} 542 543/* Embed Metadata Block */ 544.embed-meta { 545 display: flex; 546 justify-content: space-between; 547 align-items: center; 548 font-size: 0.85em; 549 color: var(--color-muted); 550 margin-top: 0.75rem; 551} 552 553.embed-stats { 554 display: flex; 555 gap: 1rem; 556 font-family: var(--font-mono); 557} 558 559.embed-stat { 560 color: var(--color-subtle); 561 font-size: 0.9em; 562} 563 564.embed-time { 565 color: var(--color-subtle); 566 text-decoration: none; 567 font-family: var(--font-mono); 568 font-size: 0.9em; 569} 570 571.embed-time:hover { 572 color: var(--color-link); 573} 574 575.embed-type { 576 font-size: 0.8em; 577 color: var(--color-subtle); 578 font-family: var(--font-mono); 579 text-transform: uppercase; 580 letter-spacing: 0.05em; 581} 582 583/* Embed URL link (shown with syntax in editor) */ 584.embed-url { 585 color: var(--color-link); 586 font-family: var(--font-mono); 587 font-size: 0.9em; 588 word-break: break-all; 589} 590 591/* External link cards */ 592.embed-external { 593 display: flex; 594 gap: 0.75rem; 595 padding: 0.75rem; 596 background: var(--color-surface); 597 border: 1px dashed var(--color-border); 598 text-decoration: none; 599 color: inherit; 600 margin-top: 0.5rem; 601} 602 603.embed-external:hover { 604 border-inline-start: 2px solid var(--color-primary); 605 margin-inline-start: -1px; 606} 607 608@media (prefers-color-scheme: dark) { 609 .embed-external { 610 border: 1px solid var(--color-border); 611 } 612 613 .embed-external:hover { 614 border-inline-start: 2px solid var(--color-primary); 615 margin-inline-start: -1px; 616 } 617} 618 619.embed-external-thumb { 620 width: 120px; 621 height: 80px; 622 object-fit: cover; 623 flex-shrink: 0; 624} 625 626.embed-external-info { 627 display: flex; 628 flex-direction: column; 629 gap: 0.25rem; 630 min-width: 0; 631} 632 633.embed-external-title { 634 font-weight: 600; 635 color: var(--color-text); 636 overflow: hidden; 637 text-overflow: ellipsis; 638 white-space: nowrap; 639} 640 641.embed-external-description { 642 font-size: 0.9em; 643 color: var(--color-muted); 644 overflow: hidden; 645 text-overflow: ellipsis; 646 display: -webkit-box; 647 -webkit-line-clamp: 2; 648 -webkit-box-orient: vertical; 649} 650 651.embed-external-url { 652 font-size: 0.8em; 653 font-family: var(--font-mono); 654 color: var(--color-subtle); 655} 656 657/* Image embeds */ 658.embed-images { 659 display: grid; 660 gap: 4px; 661 margin-top: 0.5rem; 662 overflow: hidden; 663} 664 665.embed-images-1 { 666 grid-template-columns: 1fr; 667} 668 669.embed-images-2 { 670 grid-template-columns: 1fr 1fr; 671} 672 673.embed-images-3 { 674 grid-template-columns: 1fr 1fr; 675} 676 677.embed-images-4 { 678 grid-template-columns: 1fr 1fr; 679} 680 681.embed-image-link { 682 display: block; 683 line-height: 0; 684} 685 686.embed-image { 687 width: 100%; 688 height: auto; 689 max-height: 500px; 690 object-fit: cover; 691 object-position: center; 692 margin: 0; 693} 694 695/* Quoted records */ 696.embed-quote { 697 display: block; 698 margin-top: 0.5rem; 699 padding: 0.75rem; 700 background: var(--color-overlay); 701 border-inline-start: 2px solid var(--color-tertiary); 702} 703 704@media (prefers-color-scheme: dark) { 705 .embed-quote { 706 border: 1px solid var(--color-border); 707 border-inline-start: 2px solid var(--color-tertiary); 708 } 709} 710 711.embed-quote .embed-author { 712 margin-bottom: 0.5rem; 713} 714 715.embed-quote .embed-avatar { 716 width: 24px; 717 height: 24px; 718 min-width: 24px; 719 min-height: 24px; 720 max-width: 24px; 721 max-height: 24px; 722} 723 724.embed-quote .embed-content { 725 font-size: 0.95em; 726 margin-bottom: 0; 727} 728 729/* Placeholder states */ 730.embed-video-placeholder, 731.embed-not-found, 732.embed-blocked, 733.embed-detached, 734.embed-unknown { 735 display: block; 736 padding: 1rem; 737 background: var(--color-overlay); 738 border-inline-start: 2px solid var(--color-border); 739 color: var(--color-muted); 740 font-style: italic; 741 margin-top: 0.5rem; 742 font-family: var(--font-mono); 743 font-size: 0.9em; 744} 745 746@media (prefers-color-scheme: dark) { 747 .embed-video-placeholder, 748 .embed-not-found, 749 .embed-blocked, 750 .embed-detached, 751 .embed-unknown { 752 border: 1px dashed var(--color-border); 753 } 754} 755 756/* Record card embeds (feeds, lists, labelers, starter packs) */ 757.embed-record-card { 758 display: block; 759 margin-top: 0.5rem; 760 padding: 0.75rem; 761 background: var(--color-overlay); 762 border-inline-start: 2px solid var(--color-tertiary); 763} 764 765.embed-record-card > .embed-author-name { 766 display: block; 767 font-size: 1.1em; 768} 769 770.embed-subtitle { 771 display: block; 772 font-size: 0.85em; 773 color: var(--color-muted); 774 margin-bottom: 0.5rem; 775} 776 777.embed-record-card .embed-description { 778 display: block; 779 margin: 0.5rem 0; 780} 781 782.embed-record-card .embed-stats { 783 display: block; 784 margin-top: 0.25rem; 785} 786 787/* Generic record fields */ 788.embed-fields { 789 display: block; 790 margin-top: 0.5rem; 791 font-family: var(--font-ui); 792 font-size: 0.85rem; 793 color: var(--color-muted); 794} 795 796.embed-field { 797 display: block; 798 margin-top: 0.25rem; 799} 800 801/* Nested fields get indentation */ 802.embed-fields .embed-fields { 803 display: block; 804 margin-top: 0.5rem; 805 margin-inline-start: 1rem; 806 padding-inline-start: 0.5rem; 807 border-inline-start: 1px solid var(--color-border); 808} 809 810/* Type label inside fields should be block with spacing */ 811.embed-fields > .embed-author-handle { 812 display: block; 813 margin-bottom: 0.25rem; 814} 815 816.embed-field-name { 817 color: var(--color-subtle); 818} 819 820.embed-field-number { 821 color: var(--color-tertiary); 822} 823 824.embed-field-date { 825 color: var(--color-muted); 826} 827 828.embed-field-count { 829 color: var(--color-muted); 830 font-style: italic; 831} 832 833.embed-field-bool-true { 834 color: var(--color-success); 835} 836 837.embed-field-bool-false { 838 color: var(--color-muted); 839} 840 841.embed-field-link, 842.embed-field-aturi { 843 color: var(--color-link); 844 text-decoration: none; 845} 846 847.embed-field-link:hover, 848.embed-field-aturi:hover { 849 text-decoration: underline; 850} 851 852.embed-field-did { 853 font-family: var(--font-mono); 854 font-size: 0.9em; 855} 856 857.embed-field-did .did-scheme, 858.embed-field-did .did-separator { 859 color: var(--color-muted); 860} 861 862.embed-field-did .did-method { 863 color: var(--color-tertiary); 864} 865 866.embed-field-did .did-identifier { 867 color: var(--color-text); 868} 869 870.embed-field-nsid { 871 color: var(--color-secondary); 872} 873 874.embed-field-handle { 875 color: var(--color-link); 876} 877 878/* AT URI highlighting */ 879.aturi-scheme { 880 color: var(--color-muted); 881} 882 883.aturi-slash { 884 color: var(--color-muted); 885} 886 887.aturi-authority { 888 color: var(--color-link); 889} 890 891.aturi-collection { 892 color: var(--color-secondary); 893} 894 895.aturi-rkey { 896 color: var(--color-tertiary); 897} 898 899/* Generic AT Protocol record embed */ 900.atproto-record > .embed-author-handle { 901 display: block; 902 margin-bottom: 0.25rem; 903} 904 905.atproto-record > .embed-author-name { 906 display: block; 907 margin-bottom: 0.5rem; 908} 909 910.atproto-record > .embed-content { 911 margin-bottom: 0.5rem; 912} 913 914/* Notebook entry embed - full width, expandable */ 915.atproto-entry { 916 max-width: none; 917 width: 100%; 918 margin: 1.5rem 0; 919 padding: 0; 920 background: var(--color-surface); 921 border: 1px solid var(--color-border); 922 border-inline-start: 1px solid var(--color-border); 923 box-shadow: none; 924 overflow: hidden; 925} 926 927.atproto-entry:hover { 928 border-inline-start-color: var(--color-border); 929} 930 931@media (prefers-color-scheme: dark) { 932 .atproto-entry { 933 border: 1px solid var(--color-border); 934 border-inline-start: 1px solid var(--color-border); 935 } 936} 937 938.embed-entry-header { 939 display: flex; 940 flex-wrap: wrap; 941 align-items: baseline; 942 gap: 0.5rem 1rem; 943 padding: 0.75rem 1rem; 944 background: var(--color-overlay); 945 border-bottom: 1px solid var(--color-border); 946} 947 948.embed-entry-title { 949 font-size: 1.1em; 950 font-weight: 600; 951 color: var(--color-text); 952} 953 954.embed-entry-author { 955 font-size: 0.85em; 956 color: var(--color-muted); 957} 958 959/* Hidden checkbox for expand/collapse */ 960.embed-entry-toggle { 961 display: none; 962} 963 964/* Content wrapper - scrollable when collapsed */ 965.embed-entry-content { 966 max-height: 30rem; 967 overflow-y: auto; 968 padding: 1rem; 969 transition: max-height 0.3s ease; 970} 971 972/* When checkbox is checked, expand fully */ 973.embed-entry-toggle:checked ~ .embed-entry-content { 974 max-height: none; 975} 976 977/* Expand/collapse button */ 978.embed-entry-expand { 979 display: block; 980 width: 100%; 981 padding: 0.5rem; 982 text-align: center; 983 font-size: 0.85em; 984 font-family: var(--font-ui); 985 color: var(--color-muted); 986 background: var(--color-overlay); 987 border-top: 1px solid var(--color-border); 988 cursor: pointer; 989 user-select: none; 990} 991 992.embed-entry-expand:hover { 993 color: var(--color-text); 994 background: var(--color-surface); 995} 996 997/* Toggle button text */ 998.embed-entry-expand::before { 999 content: "Expand ↓"; 1000} 1001 1002.embed-entry-toggle:checked ~ .embed-entry-expand::before { 1003 content: "Collapse ↑"; 1004} 1005 1006/* Hide expand button if content doesn't overflow (via JS class) */ 1007.atproto-entry.no-overflow .embed-entry-expand { 1008 display: none; 1009} 1010 1011/* Horizontal Rule */ 1012hr { 1013 border: none; 1014 border-top: 2px solid var(--color-border); 1015 margin: 2rem 0; 1016} 1017 1018/* Tablet and mobile responsiveness */ 1019@media (max-width: 900px) { 1020 .notebook-content { 1021 padding: 1.5rem 1rem; 1022 max-width: 100%; 1023 } 1024 1025 h1 { font-size: 1.85rem; } 1026 h2 { font-size: 1.4rem; } 1027 h3 { font-size: 1.2rem; } 1028 1029 blockquote { 1030 margin-inline-start: 0; 1031 margin-inline-end: 0; 1032 } 1033} 1034 1035/* Small mobile phones */ 1036@media (max-width: 480px) { 1037 .notebook-content { 1038 padding: 1rem 0.75rem; 1039 } 1040 1041 h1 { font-size: 1.65rem; } 1042 h2 { font-size: 1.3rem; } 1043 h3 { font-size: 1.1rem; } 1044 1045 blockquote { 1046 padding-inline-start: 0.75rem; 1047 padding-inline-end: 0.75rem; 1048 } 1049} 1050 1051/* Leaflet document embeds */ 1052.atproto-leaflet { 1053 max-width: none; 1054 width: 100%; 1055 margin: 1rem 0; 1056} 1057 1058.leaflet-document { 1059 display: block; 1060} 1061 1062.leaflet-text { 1063 margin: 0.5rem 0; 1064} 1065 1066.leaflet-button { 1067 display: inline-block; 1068 padding: 0.5rem 1rem; 1069 background: var(--color-primary); 1070 color: var(--color-base); 1071 text-decoration: none; 1072 border-radius: 4px; 1073 margin: 0.5rem 0; 1074} 1075 1076.leaflet-button:hover { 1077 opacity: 0.9; 1078} 1079 1080/* Alignment utilities */ 1081.align-center { text-align: center; } 1082.align-right { text-align: right; } 1083.align-justify { text-align: justify; } 1084 </style> 1085 <style> 1086/* Syntax highlighting - Light Mode (default) */ 1087/* 1088 * theme "Rosé Pine Dawn" generated by syntect 1089 */ 1090 1091.wvc-code { 1092 color: #575279; 1093 background-color: #faf4ed; 1094} 1095 1096.wvc-comment { 1097 color: #797593; 1098font-style: italic; 1099} 1100.wvc-string, .wvc-punctuation.wvc-definition.wvc-string { 1101 color: #ea9d34; 1102} 1103.wvc-constant.wvc-numeric { 1104 color: #ea9d34; 1105} 1106.wvc-constant.wvc-language { 1107 color: #ea9d34; 1108font-weight: bold; 1109} 1110.wvc-constant.wvc-character, .wvc-constant.wvc-other { 1111 color: #ea9d34; 1112} 1113.wvc-variable { 1114 color: #575279; 1115font-style: italic; 1116} 1117.wvc-keyword { 1118 color: #286983; 1119} 1120.wvc-storage { 1121 color: #56949f; 1122} 1123.wvc-storage.wvc-type { 1124 color: #56949f; 1125} 1126.wvc-entity.wvc-name.wvc-class { 1127 color: #286983; 1128font-weight: bold; 1129} 1130.wvc-entity.wvc-other.wvc-inherited-class { 1131 color: #286983; 1132font-style: italic; 1133} 1134.wvc-entity.wvc-name.wvc-function { 1135 color: #d7827e; 1136font-style: italic; 1137} 1138.wvc-variable.wvc-parameter { 1139 color: #907aa9; 1140} 1141.wvc-entity.wvc-name.wvc-tag { 1142 color: #286983; 1143font-weight: bold; 1144} 1145.wvc-entity.wvc-other.wvc-attribute-name { 1146 color: #907aa9; 1147} 1148.wvc-support.wvc-function { 1149 color: #d7827e; 1150font-weight: bold; 1151} 1152.wvc-support.wvc-constant { 1153 color: #ea9d34; 1154font-weight: bold; 1155} 1156.wvc-support.wvc-type, .wvc-support.wvc-class { 1157 color: #56949f; 1158font-weight: bold; 1159} 1160.wvc-support.wvc-other.wvc-variable { 1161 color: #b4637a; 1162font-weight: bold; 1163} 1164.wvc-invalid { 1165 color: #575279; 1166 background-color: #b4637a; 1167} 1168.wvc-invalid.wvc-deprecated { 1169 color: #575279; 1170 background-color: #907aa9; 1171} 1172.wvc-punctuation, .wvc-keyword.wvc-operator { 1173 color: #797593; 1174} 1175 1176 1177/* Syntax highlighting - Dark Mode */ 1178@media (prefers-color-scheme: dark) { 1179/* 1180 * theme "Rosé Pine" generated by syntect 1181 */ 1182 1183.wvc-code { 1184 color: #e0def4; 1185 background-color: #191724; 1186} 1187 1188.wvc-comment { 1189 color: #908caa; 1190font-style: italic; 1191} 1192.wvc-string, .wvc-punctuation.wvc-definition.wvc-string { 1193 color: #f6c177; 1194} 1195.wvc-constant.wvc-numeric { 1196 color: #f6c177; 1197} 1198.wvc-constant.wvc-language { 1199 color: #f6c177; 1200font-weight: bold; 1201} 1202.wvc-constant.wvc-character, .wvc-constant.wvc-other { 1203 color: #f6c177; 1204} 1205.wvc-variable { 1206 color: #e0def4; 1207font-style: italic; 1208} 1209.wvc-keyword { 1210 color: #31748f; 1211} 1212.wvc-storage { 1213 color: #9ccfd8; 1214} 1215.wvc-storage.wvc-type { 1216 color: #9ccfd8; 1217} 1218.wvc-entity.wvc-name.wvc-class { 1219 color: #31748f; 1220font-weight: bold; 1221} 1222.wvc-entity.wvc-other.wvc-inherited-class { 1223 color: #31748f; 1224font-style: italic; 1225} 1226.wvc-entity.wvc-name.wvc-function { 1227 color: #ebbcba; 1228font-style: italic; 1229} 1230.wvc-variable.wvc-parameter { 1231 color: #c4a7e7; 1232} 1233.wvc-entity.wvc-name.wvc-tag { 1234 color: #31748f; 1235font-weight: bold; 1236} 1237.wvc-entity.wvc-other.wvc-attribute-name { 1238 color: #c4a7e7; 1239} 1240.wvc-support.wvc-function { 1241 color: #ebbcba; 1242font-weight: bold; 1243} 1244.wvc-support.wvc-constant { 1245 color: #f6c177; 1246font-weight: bold; 1247} 1248.wvc-support.wvc-type, .wvc-support.wvc-class { 1249 color: #9ccfd8; 1250font-weight: bold; 1251} 1252.wvc-support.wvc-other.wvc-variable { 1253 color: #eb6f92; 1254font-weight: bold; 1255} 1256.wvc-invalid { 1257 color: #e0def4; 1258 background-color: #eb6f92; 1259} 1260.wvc-invalid.wvc-deprecated { 1261 color: #e0def4; 1262 background-color: #c4a7e7; 1263} 1264.wvc-punctuation, .wvc-keyword.wvc-operator { 1265 color: #908caa; 1266} 1267} 1268 </style> 1269</head> 1270<body style="background: var(--color-base); min-height: 100vh;"> 1271<div class="notebook-content"> 1272<h1>Weaver: Long-form Writing on AT Protocol</h1> 1273<em><p dir="ltr">Or: "Get in kid, we're rebuilding the blogosphere!"</em></p> 1274<p dir="ltr">I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal.<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Hi Rahaeli. Sorry I was the wrong kind of nerd.</span></p> 1275<blockquote> 1276<img src="weaver_photo_med.jpg" alt="weaver_photo_med.jpg" /><em><p dir="ltr">The namesake of what I'm building</em></p> 1277</blockquote> 1278<p dir="ltr">Social media in the conventional sense has been in a lot of ways a small part of the story of my time on the internet. The broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there.</p> 1279<h2>The Blogosphere</h2> 1280<p dir="ltr">I am an atheist in large part because of a blog called Common Sense Atheism.<label for="sn-2" class="sidenote-number"></label><input type="checkbox" id="sn-2" class="margin-toggle"/><span class="sidenote">The author, Luke Muehlhauser, was criticising both Richard Dawkins <em>and</em> some Christian apologetics I was familiar with.</span> Luke's blog was part of a cluster of blogs out of which grew the rationalists, one of, for better or for worse, the most influential intellectual movements of the 21st century.I also read blogs like boingboing.net, was a big fan of Cory Doctorow. I figured out I am trans in part because of Thing of Things,<label for="sn-3" class="sidenote-number"></label><input type="checkbox" id="sn-3" class="margin-toggle"/><span class="sidenote">Specifically their piece on the <a href="https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/">cluster structure of genderspace</a>.</span> a blog by Ozy Frantz, a transmasc person in the broader rationalist and Effective Altruist blogosphere.One thing these all have in common is length. Part of the reason I only really got onto Twitter in 2020 or so was because the concept of microblogging, of having to fit your thoughts into such a small package, baffled me for ages.<label for="sn-4" class="sidenote-number"></label><input type="checkbox" id="sn-4" class="margin-toggle"/><span class="sidenote">Amusingly I now think that being on Twitter and now Bluesky made me a better writer. Restrictions breed creativity, after all.</span></p> 1281<aside> 1282<blockquote> 1283<strong><p dir="ltr">On Platform Decay</strong> 1284Through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress required too much setup. Tumblr's system for comments remains insane. Hosting my own seemed like too much money to burn on something nobody might read.</p> 1285</blockquote> 1286</aside> 1287<p dir="ltr">But at the same time, Substack's success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts.</p> 1288<p dir="ltr">Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists.<label for="sn-5" class="sidenote-number"></label><input type="checkbox" id="sn-5" class="margin-toggle"/><span class="sidenote">I am very much a fan of freedom of expression. I'm not so much a fan of paying money to Nazis.</span>That's where the <code>at://</code> protocol and Weaver comes in.</p> 1289<h2>The Pitch</h2> 1290<p dir="ltr">Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto.<label for="sn-6" class="sidenote-number"></label><input type="checkbox" id="sn-6" class="margin-toggle"/><span class="sidenote">The weaver bird builds intricate, self-contained homes—seemed fitting for a platform about owning your writing.</span> I was inspired by how weaver birds build their own homes, and by the notebooks, physical and virtual, that I create in the course of my work.The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files into a static "notebook" site. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app.</p> 1291<aside> 1292<blockquote> 1293<strong><p dir="ltr">The Ultimate Goal</strong></p> 1294<p dir="ltr">Build a platform suitable for professional writers and journalists, an open alternative to platforms like Substack, with ways for readers to support writers, all on the <code>at://</code> protocol.</p> 1295</blockquote> 1296</aside> 1297<h2>How It Works</h2> 1298<p dir="ltr">Weaver works on a concept of notebooks with entries, which can be grouped into pages or chapters. They can have multiple attributed authors. You can tear out a metaphorical page and stick it in another notebook.</p> 1299<p dir="ltr">You own what you write.<label for="sn-7" class="sidenote-number"></label><input type="checkbox" id="sn-7" class="margin-toggle"/><span class="sidenote">Technically you can include entries you don't control in your notebooks, although this isn't a supported mode—it's about <em>your</em> ownership of <em>your</em> words.</span> And once collaborative editing is in, collaborative work will be resilient against deletion by one author. They can delete their notebook or even their account, but what you write will be safe.Entries are Markdown text—specifically, an extension on the Obsidian flavour of Markdown.<label for="sn-8" class="sidenote-number"></label><input type="checkbox" id="sn-8" class="margin-toggle"/><span class="sidenote">I forked the popular rust markdown processing library <code>pulldown-cmark</code> because it had limited extensibility along the axes I wanted—custom syntax extensions to support Obsidian's Markdown flavour and additional useful features, like some of the ones on show here!</span> They support additional embed types, including atproto record embeds and other markdown documents, as well as resizable images.</p> 1300<h2>Why Rust?</h2> 1301<p dir="ltr">As to why I'm writing it in Rust (and currently zero Typescript) as opposed to Go and Typescript? Well it comes down to familiarity. Rust isn't necessarily anyone's first choice in a vacuum for a web-native programming language, but it works quite well as one. I can share the vast majority of the protocol code, as well as the markdown rendering engine, between front and back end, with few if any compromises on performance, save a larger bundle size due to the nature of WebAssembly.</p> 1302<aside> 1303<blockquote> 1304<strong><p dir="ltr">On Interoperability</strong></p> 1305<p dir="ltr">The <code>at://</code> protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macroblogging" too. Weaver's app server can display Whitewind posts. With effort, it can faithfully render Leaflet posts. It doesn't care what app your profile is on.</p> 1306</blockquote> 1307</aside> 1308<h2>Evolution</h2> 1309<p dir="ltr">Weaver is therefore very much an evolving thing. It will always have and support the proof-of-concept workflow as a first-class citizen. That's part of the benefit of building this on atproto.</p> 1310<p dir="ltr">If I screw this up, not too hard for someone else to pick up the torch and continue.<label for="sn-9" class="sidenote-number"></label><input type="checkbox" id="sn-9" class="margin-toggle"/><span class="sidenote">This is the traditional footnote, at the end, because sometimes you want your citations at the bottom of the page rather than in the margins.</span></p> 1311</div> 1312</body> 1313</html>