this repo has no description
at main 21 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.0"> 6 <title>Tranquil PDS</title> 7 <style> 8 :root { 9 --space-0: 0; 10 --space-1: 0.125rem; 11 --space-2: 0.25rem; 12 --space-3: 0.5rem; 13 --space-4: 0.75rem; 14 --space-5: 1rem; 15 --space-6: 1.5rem; 16 --space-7: 2rem; 17 --space-8: 3rem; 18 --space-9: 4rem; 19 --text-xs: 0.75rem; 20 --text-sm: 0.875rem; 21 --text-base: 1rem; 22 --text-lg: 1.125rem; 23 --text-xl: 1.25rem; 24 --text-2xl: 1.5rem; 25 --text-3xl: 2rem; 26 --text-4xl: 2.5rem; 27 --font-normal: 400; 28 --font-medium: 500; 29 --font-semibold: 600; 30 --font-bold: 700; 31 --leading-tight: 1.25; 32 --leading-normal: 1.5; 33 --leading-relaxed: 1.75; 34 --radius-sm: 3px; 35 --radius-md: 4px; 36 --radius-lg: 6px; 37 --radius-xl: 8px; 38 --width-xs: 360px; 39 --width-sm: 480px; 40 --width-md: 760px; 41 --width-lg: 960px; 42 --width-xl: 1100px; 43 --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); 44 --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1); 45 --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); 46 --shadow-focus: 0 0 0 2px var(--accent-muted); 47 --transition-fast: 0.1s ease; 48 --transition-normal: 0.15s ease; 49 --transition-slow: 0.25s ease; 50 --font-mono: ui-monospace, "SF Mono", Menlo, Monaco, monospace; 51 --bg-primary: #f9fafa; 52 --bg-secondary: #f1f3f3; 53 --bg-tertiary: #e8ebeb; 54 --bg-hover: #e8ebeb; 55 --bg-card: #ffffff; 56 --bg-input: #ffffff; 57 --bg-input-disabled: #f1f3f3; 58 --text-primary: #1a1d1d; 59 --text-secondary: #5a605f; 60 --text-muted: #8a8f8e; 61 --text-inverse: #ffffff; 62 --border-color: #dce0df; 63 --border-light: #e8ebeb; 64 --border-dark: #c8cecc; 65 --accent: #1a1d1d; 66 --accent-hover: #2e3332; 67 --accent-muted: rgba(26, 29, 29, 0.06); 68 --accent-light: #3a403f; 69 --secondary: #1a1d1d; 70 --secondary-hover: #2e3332; 71 --secondary-muted: rgba(26, 29, 29, 0.06); 72 --success-bg: #dfd; 73 --success-border: #8c8; 74 --success-text: #060; 75 --error-bg: #fee; 76 --error-border: #fcc; 77 --error-text: #c00; 78 --warning-bg: #ffd; 79 --warning-border: #d4a03c; 80 --warning-text: #856404; 81 --border-color-light: var(--border-dark); 82 } 83 @media (prefers-color-scheme: dark) { 84 :root { 85 --bg-primary: #0a0c0c; 86 --bg-secondary: #131616; 87 --bg-tertiary: #1a1d1d; 88 --bg-hover: #1a1d1d; 89 --bg-card: #131616; 90 --bg-input: #1a1d1d; 91 --bg-input-disabled: #131616; 92 --text-primary: #e6e8e8; 93 --text-secondary: #9ca1a0; 94 --text-muted: #686d6c; 95 --text-inverse: #0a0c0c; 96 --border-color: #282c2b; 97 --border-light: #1f2322; 98 --border-dark: #343938; 99 --accent: #e6e8e8; 100 --accent-hover: #ffffff; 101 --accent-muted: rgba(230, 232, 232, 0.1); 102 --accent-light: #ffffff; 103 --secondary: #e6e8e8; 104 --secondary-hover: #ffffff; 105 --secondary-muted: rgba(230, 232, 232, 0.1); 106 --success-bg: #0f1f1a; 107 --success-border: #1a3d2d; 108 --success-text: #7bc6a0; 109 --error-bg: #1f0f0f; 110 --error-border: #3d1a1a; 111 --error-text: #ff8a8a; 112 --warning-bg: #1f1a0f; 113 --warning-border: #3d351a; 114 --warning-text: #c6b87b; 115 } 116 } 117 118 *, *::before, *::after { 119 box-sizing: border-box; 120 } 121 body { 122 margin: 0; 123 font-family: 124 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 125 sans-serif; 126 background: var(--bg-primary); 127 color: var(--text-primary); 128 line-height: var(--leading-normal); 129 -webkit-font-smoothing: antialiased; 130 } 131 132 .pattern-container { 133 position: fixed; 134 top: -32px; 135 left: -32px; 136 right: -32px; 137 bottom: -32px; 138 pointer-events: none; 139 z-index: 1; 140 overflow: hidden; 141 } 142 .pattern { 143 position: absolute; 144 top: 0; 145 left: 0; 146 width: calc(100% + 500px); 147 height: 100%; 148 animation: drift 80s linear infinite; 149 } 150 .dot { 151 position: absolute; 152 width: 10px; 153 height: 10px; 154 background: rgba(0, 0, 0, 0.06); 155 border-radius: 50%; 156 transition: transform 0.04s linear; 157 } 158 @media (prefers-color-scheme: dark) { 159 .dot { 160 background: rgba(255, 255, 255, 0.1); 161 } 162 } 163 .pattern-fade { 164 position: fixed; 165 top: 0; 166 left: 0; 167 right: 0; 168 bottom: 0; 169 background: linear-gradient( 170 135deg, 171 transparent 50%, 172 var(--bg-primary) 75% 173 ); 174 pointer-events: none; 175 z-index: 2; 176 } 177 @keyframes drift { 178 0% { 179 transform: translateX(-500px); 180 } 181 100% { 182 transform: translateX(0); 183 } 184 } 185 186 nav { 187 position: fixed; 188 top: 12px; 189 left: 32px; 190 right: 32px; 191 background: var(--accent); 192 padding: 10px 18px; 193 z-index: 100; 194 border-radius: var(--radius-xl); 195 display: flex; 196 justify-content: space-between; 197 align-items: center; 198 } 199 .nav-left { 200 display: flex; 201 align-items: center; 202 gap: var(--space-3); 203 } 204 .nav-logo { 205 height: 28px; 206 width: auto; 207 object-fit: contain; 208 border-radius: var(--radius-sm); 209 } 210 .hostname { 211 font-weight: var(--font-semibold); 212 font-size: var(--text-base); 213 letter-spacing: 0.08em; 214 color: var(--text-inverse); 215 text-transform: uppercase; 216 } 217 .hostname.placeholder { 218 opacity: 0.4; 219 } 220 .user-count { 221 font-size: var(--text-sm); 222 color: var(--text-inverse); 223 opacity: 0.85; 224 padding: 4px 10px; 225 background: rgba(255, 255, 255, 0.15); 226 border-radius: var(--radius-md); 227 white-space: nowrap; 228 } 229 @media (prefers-color-scheme: dark) { 230 .user-count { 231 background: rgba(0, 0, 0, 0.15); 232 } 233 } 234 .nav-meta { 235 font-size: var(--text-sm); 236 color: var(--text-inverse); 237 opacity: 0.6; 238 letter-spacing: 0.05em; 239 } 240 241 .home { 242 position: relative; 243 z-index: 10; 244 max-width: var(--width-xl); 245 margin: 0 auto; 246 padding: 72px 32px 32px; 247 } 248 .hero { 249 padding: var(--space-7) 0 var(--space-8); 250 border-bottom: 1px solid var(--border-color); 251 margin-bottom: var(--space-8); 252 } 253 h1 { 254 font-size: var(--text-4xl); 255 font-weight: var(--font-semibold); 256 line-height: var(--leading-tight); 257 margin-bottom: var(--space-6); 258 letter-spacing: -0.02em; 259 } 260 .cycling-word-container { 261 display: inline-block; 262 width: 3.9em; 263 text-align: left; 264 } 265 .cycling-word { 266 display: inline-block; 267 transition: opacity 0.1s ease, transform 0.1s ease; 268 } 269 .cycling-word.transitioning { 270 opacity: 0; 271 transform: scale(0.95); 272 } 273 .lede { 274 font-size: var(--text-xl); 275 font-weight: var(--font-medium); 276 color: var(--text-primary); 277 line-height: var(--leading-relaxed); 278 margin-bottom: 0; 279 } 280 .actions { 281 display: flex; 282 gap: var(--space-4); 283 margin-top: var(--space-7); 284 } 285 .btn { 286 font-size: var(--text-sm); 287 font-weight: var(--font-medium); 288 text-transform: uppercase; 289 letter-spacing: 0.06em; 290 padding: var(--space-4) var(--space-6); 291 border-radius: var(--radius-lg); 292 text-decoration: none; 293 transition: all var(--transition-normal); 294 border: 1px solid transparent; 295 } 296 .btn.primary { 297 background: var(--secondary); 298 color: var(--text-inverse); 299 border-color: var(--secondary); 300 } 301 .btn.primary:hover { 302 background: var(--secondary-hover); 303 border-color: var(--secondary-hover); 304 } 305 .btn.secondary { 306 background: transparent; 307 color: var(--text-primary); 308 border-color: var(--border-color); 309 } 310 .btn.secondary:hover { 311 background: var(--secondary-muted); 312 border-color: var(--secondary); 313 color: var(--secondary); 314 } 315 blockquote { 316 margin: var(--space-8) 0 0 0; 317 padding: var(--space-6); 318 background: var(--accent-muted); 319 border-left: 3px solid var(--accent); 320 border-radius: 0 var(--radius-xl) var(--radius-xl) 0; 321 } 322 blockquote p { 323 font-size: var(--text-lg); 324 color: var(--text-primary); 325 font-style: italic; 326 margin-bottom: var(--space-3); 327 } 328 blockquote cite { 329 font-size: var(--text-sm); 330 color: var(--text-secondary); 331 font-style: normal; 332 text-transform: uppercase; 333 letter-spacing: 0.05em; 334 } 335 .content h2 { 336 font-size: var(--text-sm); 337 font-weight: var(--font-bold); 338 text-transform: uppercase; 339 letter-spacing: 0.1em; 340 color: var(--accent-light); 341 margin: var(--space-8) 0 var(--space-5); 342 } 343 .content h2:first-child { 344 margin-top: 0; 345 } 346 .content > p { 347 font-size: var(--text-base); 348 color: var(--text-secondary); 349 margin-bottom: var(--space-5); 350 line-height: var(--leading-relaxed); 351 } 352 .features { 353 display: grid; 354 grid-template-columns: repeat(2, 1fr); 355 gap: var(--space-6); 356 margin: var(--space-6) 0 var(--space-8); 357 } 358 .feature { 359 padding: var(--space-5); 360 background: var(--bg-secondary); 361 border-radius: var(--radius-xl); 362 border: 1px solid var(--border-color); 363 } 364 .feature h3 { 365 font-size: var(--text-base); 366 font-weight: var(--font-semibold); 367 color: var(--text-primary); 368 margin-bottom: var(--space-3); 369 } 370 .feature p { 371 font-size: var(--text-sm); 372 color: var(--text-secondary); 373 margin: 0; 374 line-height: var(--leading-relaxed); 375 } 376 @media (max-width: 700px) { 377 .features { 378 grid-template-columns: 1fr; 379 } 380 h1 { 381 font-size: var(--text-3xl); 382 } 383 .actions { 384 flex-direction: column; 385 } 386 .btn { 387 text-align: center; 388 } 389 .user-count, .nav-meta { 390 display: none; 391 } 392 } 393 .site-footer { 394 margin-top: var(--space-9); 395 padding-top: var(--space-7); 396 display: flex; 397 justify-content: space-between; 398 font-size: var(--text-sm); 399 color: var(--text-muted); 400 text-transform: uppercase; 401 letter-spacing: 0.05em; 402 border-top: 1px solid var(--border-color); 403 } 404 .hidden { 405 display: none !important; 406 } 407 </style> 408 </head> 409 <body> 410 <div class="pattern-container"> 411 <div class="pattern" id="dotPattern"></div> 412 </div> 413 <div class="pattern-fade"></div> 414 415 <nav> 416 <div class="nav-left"> 417 <img src="/logo" alt="Logo" class="nav-logo hidden" id="navLogo"> 418 <span class="hostname" id="hostname">loading...</span> 419 <span class="user-count hidden" id="userCount"></span> 420 </div> 421 <span class="nav-meta" id="version"></span> 422 </nav> 423 424 <div class="home"> 425 <section class="hero"> 426 <h1> 427 A home for your <span class="cycling-word-container"><span 428 class="cycling-word" 429 id="cyclingWord" 430 >Bluesky</span></span> account 431 </h1> 432 433 <p class="lede"> 434 Tranquil PDS is a Personal Data Server, the thing that stores your 435 posts, profile, and keys. Bluesky runs one for you, but you can run 436 your own. 437 </p> 438 439 <div class="actions" id="heroActions"> 440 <a href="/app/register" class="btn primary" id="heroPrimary" 441 >Join This Server</a> 442 <a 443 href="https://tangled.org/lewis.moe/bspds-sandbox" 444 class="btn secondary" 445 id="heroSecondary" 446 target="_blank" 447 rel="noopener" 448 >Run Your Own</a> 449 </div> 450 451 <blockquote> 452 <p>"Nature does not hurry, yet everything is accomplished."</p> 453 <cite>Lao Tzu</cite> 454 </blockquote> 455 </section> 456 457 <section class="content"> 458 <h2>What you get</h2> 459 460 <div class="features"> 461 <div class="feature"> 462 <h3>Real security</h3> 463 <p> 464 Sign in with passkeys, add two-factor authentication, set up 465 backup codes, and mark devices you trust. Your account stays 466 yours. 467 </p> 468 </div> 469 470 <div class="feature"> 471 <h3>Your own identity</h3> 472 <p> 473 Use your own domain as your handle, or get a subdomain on ours. 474 Either way, your identity moves with you if you ever leave. 475 </p> 476 </div> 477 478 <div class="feature"> 479 <h3>Stay in the loop</h3> 480 <p> 481 Get important alerts where you actually see them: email, Discord, 482 Telegram, or Signal. 483 </p> 484 </div> 485 486 <div class="feature"> 487 <h3>You decide what apps can do</h3> 488 <p> 489 When an app asks for access, you'll see exactly what it wants in 490 plain language. Grant what makes sense, deny what doesn't. 491 </p> 492 </div> 493 494 <div class="feature"> 495 <h3>App passwords with guardrails</h3> 496 <p> 497 Create app passwords that can only do specific things: read-only 498 for feed readers, post-only for bots. Full control over what each 499 password can access. 500 </p> 501 </div> 502 503 <div class="feature"> 504 <h3>Delegate without sharing passwords</h3> 505 <p> 506 Let team members or tools manage your account with specific 507 permission levels. They authenticate with their own credentials, 508 you see everything they do in an audit log. 509 </p> 510 </div> 511 512 <div class="feature"> 513 <h3>Automatic backups</h3> 514 <p> 515 Your repository is backed up daily to object storage. Download any 516 backup or restore with one click. You own your data, even if the 517 worst happens. 518 </p> 519 </div> 520 </div> 521 522 <h2>Everything in one place</h2> 523 524 <p> 525 Manage your profile, security settings, connected apps, and more from 526 a clean dashboard. No command line or 3rd party apps required. 527 </p> 528 529 <h2>Works with everything</h2> 530 531 <p> 532 Use any ATProto app you already like. Tranquil PDS speaks the same 533 language as Bluesky's servers, so all your favorite clients and tools 534 just work. 535 </p> 536 537 <h2>Ready to try it?</h2> 538 539 <p> 540 Join this server, or grab the source and run your own. Either way, you 541 can migrate an existing account over and your followers, posts, and 542 identity come with you. 543 </p> 544 545 <div class="actions" id="footerActions"> 546 <a href="/app/register" class="btn primary" id="footerPrimary" 547 >Join This Server</a> 548 <a 549 href="https://tangled.org/lewis.moe/bspds-sandbox" 550 class="btn secondary" 551 target="_blank" 552 rel="noopener" 553 >View Source</a> 554 </div> 555 </section> 556 557 <footer class="site-footer"> 558 <span>Made by people who don't take themselves too seriously</span> 559 <span>Open Source: issues & PRs welcome</span> 560 </footer> 561 </div> 562 563 <script> 564 (function checkSession() { 565 try { 566 const stored = localStorage.getItem("tranquil_pds_session"); 567 if (stored) { 568 const session = JSON.parse(stored); 569 if (session && session.handle) { 570 const handle = "@" + session.handle; 571 const heroPrimary = document.getElementById( 572 "heroPrimary", 573 ); 574 const footerPrimary = document.getElementById( 575 "footerPrimary", 576 ); 577 const heroSecondary = document.getElementById( 578 "heroSecondary", 579 ); 580 if (heroPrimary) { 581 heroPrimary.href = "/app/dashboard"; 582 heroPrimary.textContent = handle; 583 } 584 if (footerPrimary) { 585 footerPrimary.href = "/app/dashboard"; 586 footerPrimary.textContent = handle; 587 } 588 if (heroSecondary) { 589 heroSecondary.classList.add("hidden"); 590 } 591 } 592 } 593 } catch (e) {} 594 })(); 595 596 const heroWords = ["Bluesky", "Tangled", "Leaflet", "ATProto"]; 597 const wordSpacing = { 598 "Bluesky": "0.01em", 599 "Tangled": "0.02em", 600 "Leaflet": "0.05em", 601 "ATProto": "0", 602 }; 603 let currentWordIndex = 0; 604 const cyclingWord = document.getElementById("cyclingWord"); 605 606 function cycleWord() { 607 cyclingWord.classList.add("transitioning"); 608 setTimeout(() => { 609 currentWordIndex = (currentWordIndex + 1) % heroWords.length; 610 const word = heroWords[currentWordIndex]; 611 cyclingWord.textContent = word; 612 cyclingWord.style.letterSpacing = wordSpacing[word] || "0"; 613 cyclingWord.classList.remove("transitioning"); 614 const duration = word === "ATProto" ? 4000 : 2000; 615 setTimeout(cycleWord, duration); 616 }, 100); 617 } 618 setTimeout(cycleWord, 2000); 619 620 fetch("/xrpc/com.atproto.server.describeServer") 621 .then((r) => r.json()) 622 .then((info) => { 623 if (info.availableUserDomains?.length) { 624 document.getElementById("hostname").textContent = 625 info.availableUserDomains[0]; 626 document.getElementById("hostname").classList.remove( 627 "placeholder", 628 ); 629 } 630 if (info.version) { 631 document.getElementById("version").textContent = 632 info.version; 633 } 634 }) 635 .catch(() => {}); 636 637 fetch("/xrpc/com.atproto.sync.listRepos?limit=1000") 638 .then((r) => r.json()) 639 .then((data) => { 640 const count = data.repos?.length || 0; 641 const el = document.getElementById("userCount"); 642 el.textContent = count + " " + 643 (count === 1 ? "user" : "users"); 644 el.classList.remove("hidden"); 645 }) 646 .catch(() => {}); 647 648 fetch("/logo", { method: "HEAD" }) 649 .then((r) => { 650 if (r.ok) { 651 document.getElementById("navLogo").classList.remove( 652 "hidden", 653 ); 654 } 655 }) 656 .catch(() => {}); 657 658 const pattern = document.getElementById("dotPattern"); 659 const spacing = 32; 660 const cols = Math.ceil((window.innerWidth + 600) / spacing); 661 const rows = Math.ceil((window.innerHeight + 100) / spacing); 662 const dots = []; 663 664 for (let y = 0; y < rows; y++) { 665 for (let x = 0; x < cols; x++) { 666 const dot = document.createElement("div"); 667 dot.className = "dot"; 668 dot.style.left = (x * spacing) + "px"; 669 dot.style.top = (y * spacing) + "px"; 670 pattern.appendChild(dot); 671 dots.push({ el: dot, x: x * spacing, y: y * spacing }); 672 } 673 } 674 675 let mouseX = -1000, mouseY = -1000; 676 document.addEventListener("mousemove", (e) => { 677 mouseX = e.clientX; 678 mouseY = e.clientY; 679 }); 680 681 function updateDots() { 682 const patternRect = pattern.getBoundingClientRect(); 683 dots.forEach((dot) => { 684 const dotX = patternRect.left + dot.x + 5; 685 const dotY = patternRect.top + dot.y + 5; 686 const dist = Math.hypot(mouseX - dotX, mouseY - dotY); 687 const maxDist = 120; 688 const scale = Math.min(1, Math.max(0.1, dist / maxDist)); 689 dot.el.style.transform = "scale(" + scale + ")"; 690 }); 691 requestAnimationFrame(updateDots); 692 } 693 updateDots(); 694 </script> 695 </body> 696</html>