this repo has no description

Some actual styling

lewis 53fa41b9 13bf4455

+5 -2
frontend/index.html
··· 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>Tranquil PDS</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com"> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 + <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> 7 10 <style> 8 - html { background: #fafafa; } 9 - @media (prefers-color-scheme: dark) { html { background: #1a1a1a; } } 11 + html { background: #ffffff; } 12 + @media (prefers-color-scheme: dark) { html { background: #0a0a0a; } } 10 13 </style> 11 14 </head> 12 15 <body>
+650
frontend/mockups/01-article-style.html
··· 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</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com"> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 + <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 10 + <style> 11 + * { margin: 0; padding: 0; box-sizing: border-box; } 12 + 13 + body { 14 + font-family: 'JetBrains Mono', monospace; 15 + line-height: 1.7; 16 + background: #2c00ff; 17 + color: #ffffff; 18 + min-height: 100vh; 19 + position: relative; 20 + } 21 + 22 + .pattern-container { 23 + position: fixed; 24 + top: -32px; 25 + left: -32px; 26 + right: -32px; 27 + bottom: -32px; 28 + pointer-events: none; 29 + z-index: 1; 30 + overflow: hidden; 31 + } 32 + 33 + .pattern { 34 + position: absolute; 35 + top: 0; 36 + left: 0; 37 + width: calc(100% + 500px); 38 + height: 100%; 39 + animation: drift 80s linear infinite; 40 + } 41 + 42 + .dot { 43 + position: absolute; 44 + width: 10px; 45 + height: 10px; 46 + background: rgba(255,255,255,0.15); 47 + border-radius: 50%; 48 + transition: transform 0.04s linear; 49 + } 50 + 51 + .pattern-fade { 52 + position: fixed; 53 + top: 0; 54 + left: 0; 55 + right: 0; 56 + bottom: 0; 57 + background: linear-gradient(135deg, transparent 50%, #2c00ff 75%); 58 + pointer-events: none; 59 + z-index: 2; 60 + } 61 + 62 + @keyframes drift { 63 + 0% { transform: translateX(-500px); } 64 + 100% { transform: translateX(0); } 65 + } 66 + 67 + nav { z-index: 100; } 68 + main { position: relative; z-index: 10; } 69 + .site-footer { position: relative; z-index: 10; } 70 + 71 + a { color: #ff2400; text-decoration: none; } 72 + a:hover { color: #ff5533; } 73 + 74 + nav { 75 + position: fixed; 76 + top: 12px; 77 + left: 32px; 78 + right: 32px; 79 + background: #1a00a3; 80 + padding: 10px 18px; 81 + z-index: 100; 82 + border-radius: 8px; 83 + border: 1px solid rgba(255, 255, 255, 0.1); 84 + display: flex; 85 + justify-content: space-between; 86 + align-items: center; 87 + } 88 + 89 + nav .brand { 90 + font-weight: 600; 91 + font-size: 1rem; 92 + letter-spacing: 0.08em; 93 + color: #ffffff; 94 + text-transform: uppercase; 95 + } 96 + 97 + nav .nav-meta { 98 + font-size: 0.85rem; 99 + color: rgba(255, 255, 255, 0.7); 100 + letter-spacing: 0.05em; 101 + } 102 + 103 + main { 104 + max-width: 1000px; 105 + margin: 0 auto; 106 + padding: 80px 32px 80px; 107 + } 108 + 109 + .meta { 110 + display: flex; 111 + align-items: center; 112 + gap: 16px; 113 + margin-bottom: 32px; 114 + font-size: 0.8rem; 115 + font-weight: 500; 116 + text-transform: uppercase; 117 + letter-spacing: 0.1em; 118 + } 119 + 120 + .category { 121 + color: #ff2400; 122 + background: rgba(255, 255, 255, 0.95); 123 + padding: 4px 10px; 124 + border-radius: 4px; 125 + } 126 + 127 + .read-time { 128 + color: rgba(255, 255, 255, 0.8); 129 + } 130 + 131 + h1 { 132 + font-size: 2.75rem; 133 + font-weight: 600; 134 + line-height: 1.15; 135 + color: #ffffff; 136 + margin-bottom: 32px; 137 + letter-spacing: -0.02em; 138 + } 139 + 140 + .byline { 141 + display: flex; 142 + align-items: center; 143 + gap: 16px; 144 + padding: 24px 0; 145 + border-top: 1px solid rgba(255, 255, 255, 0.12); 146 + border-bottom: 1px solid rgba(255, 255, 255, 0.12); 147 + margin-bottom: 48px; 148 + } 149 + 150 + .avatar { 151 + width: 44px; 152 + height: 44px; 153 + border-radius: 50%; 154 + background: linear-gradient(135deg, #ff2400 0%, #ff6b4a 100%); 155 + } 156 + 157 + .author-info { 158 + flex: 1; 159 + } 160 + 161 + .author { 162 + display: block; 163 + font-weight: 500; 164 + color: #ffffff; 165 + font-size: 1rem; 166 + } 167 + 168 + .author-handle { 169 + display: block; 170 + font-size: 0.85rem; 171 + color: rgba(255, 255, 255, 0.8); 172 + margin-top: 2px; 173 + } 174 + 175 + .verification { 176 + font-size: 0.75rem; 177 + font-weight: 500; 178 + color: rgba(255, 255, 255, 0.85); 179 + text-transform: uppercase; 180 + letter-spacing: 0.08em; 181 + } 182 + 183 + .placeholder-image { 184 + aspect-ratio: 16 / 9; 185 + background: rgba(255, 255, 255, 0.08); 186 + border-radius: 8px; 187 + display: flex; 188 + align-items: center; 189 + justify-content: center; 190 + font-size: 0.9rem; 191 + color: rgba(255, 255, 255, 0.6); 192 + text-transform: uppercase; 193 + letter-spacing: 0.1em; 194 + border: 1px solid rgba(255, 255, 255, 0.15); 195 + } 196 + 197 + figcaption { 198 + margin-top: 12px; 199 + font-size: 0.85rem; 200 + color: rgba(255, 255, 255, 0.75); 201 + text-align: center; 202 + } 203 + 204 + .carousel { 205 + margin: 64px 0 0; 206 + } 207 + 208 + .carousel-header { 209 + display: flex; 210 + justify-content: space-between; 211 + align-items: center; 212 + margin-bottom: 20px; 213 + } 214 + 215 + .carousel-title { 216 + font-size: 0.85rem; 217 + font-weight: 600; 218 + text-transform: uppercase; 219 + letter-spacing: 0.1em; 220 + color: #ffffff; 221 + } 222 + 223 + .carousel-nav { 224 + display: flex; 225 + gap: 8px; 226 + } 227 + 228 + .carousel-nav button { 229 + font-family: 'JetBrains Mono', monospace; 230 + width: 36px; 231 + height: 36px; 232 + background: rgba(255, 255, 255, 0.08); 233 + border: 1px solid rgba(255, 255, 255, 0.15); 234 + border-radius: 6px; 235 + color: #ffffff; 236 + cursor: pointer; 237 + transition: all 0.15s ease; 238 + font-size: 1rem; 239 + } 240 + 241 + .carousel-nav button:hover { 242 + background: rgba(255, 36, 0, 0.15); 243 + border-color: #ff2400; 244 + } 245 + 246 + .carousel-track { 247 + display: flex; 248 + gap: 16px; 249 + overflow-x: auto; 250 + scroll-snap-type: x mandatory; 251 + scrollbar-width: none; 252 + -ms-overflow-style: none; 253 + padding-bottom: 8px; 254 + -webkit-overflow-scrolling: touch; 255 + user-select: none; 256 + } 257 + 258 + .carousel-track::-webkit-scrollbar { 259 + display: none; 260 + } 261 + 262 + .carousel-slide { 263 + flex: 0 0 70%; 264 + scroll-snap-align: start; 265 + } 266 + 267 + .carousel-slide .placeholder-image { 268 + aspect-ratio: 16 / 10; 269 + } 270 + 271 + .carousel-label { 272 + margin-top: 12px; 273 + font-size: 0.8rem; 274 + font-weight: 500; 275 + color: rgba(255, 255, 255, 0.85); 276 + text-transform: uppercase; 277 + letter-spacing: 0.08em; 278 + } 279 + 280 + .content { 281 + font-size: 1.05rem; 282 + font-weight: 400; 283 + } 284 + 285 + .content p { 286 + margin-bottom: 28px; 287 + } 288 + 289 + .lede { 290 + font-size: 1.3rem; 291 + font-weight: 500; 292 + color: #ffffff; 293 + line-height: 1.5; 294 + } 295 + 296 + .content h2 { 297 + font-size: 0.9rem; 298 + font-weight: 600; 299 + text-transform: uppercase; 300 + letter-spacing: 0.1em; 301 + color: #ffffff; 302 + margin: 56px 0 24px; 303 + } 304 + 305 + blockquote { 306 + margin: 40px 0; 307 + padding: 32px; 308 + background: rgba(255, 255, 255, 0.05); 309 + border-left: 2px solid #ff2400; 310 + border-radius: 0 8px 8px 0; 311 + } 312 + 313 + blockquote p { 314 + font-size: 1.15rem; 315 + color: #ffffff; 316 + font-style: italic; 317 + margin-bottom: 16px !important; 318 + } 319 + 320 + blockquote cite { 321 + font-size: 0.8rem; 322 + color: rgba(255, 255, 255, 0.8); 323 + font-style: normal; 324 + text-transform: uppercase; 325 + letter-spacing: 0.05em; 326 + } 327 + 328 + .context-panel { 329 + margin: 40px 0; 330 + padding: 24px; 331 + background: rgba(255, 255, 255, 0.05); 332 + border-radius: 8px; 333 + border: 1px solid rgba(255, 255, 255, 0.1); 334 + } 335 + 336 + .context-panel h3 { 337 + font-size: 0.8rem; 338 + font-weight: 600; 339 + text-transform: uppercase; 340 + letter-spacing: 0.1em; 341 + color: #ffffff; 342 + margin-bottom: 16px; 343 + } 344 + 345 + .context-panel ul { 346 + list-style: none; 347 + } 348 + 349 + .context-panel li { 350 + padding: 10px 0; 351 + border-bottom: 1px solid rgba(255, 255, 255, 0.1); 352 + } 353 + 354 + .context-panel li:last-child { 355 + border-bottom: none; 356 + } 357 + 358 + .context-panel a { 359 + font-size: 0.95rem; 360 + font-weight: 500; 361 + color: #ff2400; 362 + text-decoration: none; 363 + transition: color 0.15s ease; 364 + } 365 + 366 + .context-panel a:hover { 367 + color: #ff5533; 368 + } 369 + 370 + .article-footer { 371 + margin-top: 64px; 372 + padding-top: 32px; 373 + border-top: 1px solid rgba(255, 255, 255, 0.12); 374 + } 375 + 376 + .actions { 377 + display: flex; 378 + gap: 12px; 379 + margin-bottom: 24px; 380 + } 381 + 382 + .actions button { 383 + font-family: 'JetBrains Mono', monospace; 384 + font-size: 0.85rem; 385 + font-weight: 500; 386 + text-transform: uppercase; 387 + letter-spacing: 0.06em; 388 + padding: 14px 24px; 389 + background: rgba(255, 255, 255, 0.06); 390 + border: 1px solid rgba(255, 255, 255, 0.12); 391 + border-radius: 6px; 392 + color: #ffffff; 393 + cursor: pointer; 394 + transition: all 0.15s ease; 395 + } 396 + 397 + .actions button:hover { 398 + background: rgba(255, 36, 0, 0.15); 399 + border-color: #ff2400; 400 + color: #ffffff; 401 + } 402 + 403 + .attestation-info { 404 + display: flex; 405 + flex-wrap: wrap; 406 + gap: 24px; 407 + font-size: 0.8rem; 408 + color: rgba(255, 255, 255, 0.7); 409 + text-transform: uppercase; 410 + letter-spacing: 0.05em; 411 + } 412 + 413 + .site-footer { 414 + max-width: 1000px; 415 + margin: 0 auto; 416 + padding: 48px 32px; 417 + display: flex; 418 + justify-content: space-between; 419 + font-size: 0.8rem; 420 + color: rgba(255, 255, 255, 0.65); 421 + text-transform: uppercase; 422 + letter-spacing: 0.05em; 423 + border-top: 1px solid rgba(255, 255, 255, 0.12); 424 + } 425 + 426 + ::selection { 427 + background: rgba(255, 36, 0, 0.4); 428 + } 429 + </style> 430 + </head> 431 + <body> 432 + 433 + <div class="pattern-container"> 434 + <div class="pattern"></div> 435 + </div> 436 + <div class="pattern-fade"></div> 437 + 438 + <nav> 439 + <span class="brand">Tranquil</span> 440 + <span class="nav-meta">0.1.0</span> 441 + </nav> 442 + 443 + <main> 444 + <article> 445 + <div class="meta"> 446 + <span class="category">Landing page</span> 447 + <span class="read-time">1 min read</span> 448 + </div> 449 + 450 + <h1>Lorem Ipsum Dolor Sit Amet Consectetur</h1> 451 + 452 + <div class="byline"> 453 + <div class="avatar"></div> 454 + <div class="author-info"> 455 + <span class="author">Mysterious benefactor</span> 456 + <span class="author-handle">@lewis.moe</span> 457 + </div> 458 + <div class="verification">47 attestations</div> 459 + </div> 460 + 461 + <div class="content"> 462 + <blockquote> 463 + <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p> 464 + <cite>Cicero, De Finibus Bonorum et Malorum</cite> 465 + </blockquote> 466 + 467 + <p class="lede">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.</p> 468 + 469 + <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p> 470 + 471 + <p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p> 472 + 473 + <h2>Neque Porro Quisquam</h2> 474 + 475 + <p>Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p> 476 + 477 + <p>Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.</p> 478 + 479 + <h2>Quis Autem Vel Eum</h2> 480 + 481 + <p>Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.</p> 482 + 483 + <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.</p> 484 + 485 + <p>Similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.</p> 486 + 487 + <p>Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.</p> 488 + 489 + <p>Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.</p> 490 + 491 + <div class="carousel"> 492 + <div class="carousel-header"> 493 + <span class="carousel-title">Interface</span> 494 + <div class="carousel-nav"> 495 + <button class="carousel-prev">←</button> 496 + <button class="carousel-next">→</button> 497 + </div> 498 + </div> 499 + <div class="carousel-track"> 500 + <div class="carousel-slide"> 501 + <div class="placeholder-image">Dashboard goes here</div> 502 + <div class="carousel-label">Dashboard</div> 503 + </div> 504 + <div class="carousel-slide"> 505 + <div class="placeholder-image">Profile Settings go here</div> 506 + <div class="carousel-label">Profile Settings</div> 507 + </div> 508 + <div class="carousel-slide"> 509 + <div class="placeholder-image">Account Security goes here</div> 510 + <div class="carousel-label">Account Security</div> 511 + </div> 512 + <div class="carousel-slide"> 513 + <div class="placeholder-image">Repository Browser goes here</div> 514 + <div class="carousel-label">Repository Browser</div> 515 + </div> 516 + <div class="carousel-slide"> 517 + <div class="placeholder-image">OAuth Applications go here</div> 518 + <div class="carousel-label">OAuth Applications</div> 519 + </div> 520 + <div class="carousel-slide"> 521 + <div class="placeholder-image">Invite Codes go here</div> 522 + <div class="carousel-label">Invite Codes</div> 523 + </div> 524 + </div> 525 + </div> 526 + </div> 527 + 528 + <footer class="article-footer"> 529 + <div class="actions"> 530 + <button>Propagate</button> 531 + <button>Annotate</button> 532 + <button>Verify Source</button> 533 + </div> 534 + 535 + <div class="attestation-info"> 536 + <span>hash: 7f3a9c...</span> 537 + <span>signed: 2847.12.03</span> 538 + <span>nodes: 12,847</span> 539 + </div> 540 + </footer> 541 + </article> 542 + </main> 543 + 544 + <footer class="site-footer"> 545 + <div>Mesh Commons License</div> 546 + <div>node: local-7f3a</div> 547 + </footer> 548 + 549 + <script> 550 + const pattern = document.querySelector('.pattern'); 551 + const spacing = 32; 552 + const cols = Math.ceil((window.innerWidth + 600) / spacing); 553 + const rows = Math.ceil((window.innerHeight + 100) / spacing); 554 + const dots = []; 555 + 556 + for (let y = 0; y < rows; y++) { 557 + for (let x = 0; x < cols; x++) { 558 + const dot = document.createElement('div'); 559 + dot.className = 'dot'; 560 + dot.style.left = (x * spacing) + 'px'; 561 + dot.style.top = (y * spacing) + 'px'; 562 + pattern.appendChild(dot); 563 + dots.push({ el: dot, x: x * spacing, y: y * spacing }); 564 + } 565 + } 566 + 567 + let mouseX = -1000, mouseY = -1000; 568 + document.addEventListener('mousemove', e => { 569 + mouseX = e.clientX; 570 + mouseY = e.clientY; 571 + }); 572 + 573 + function updateDots() { 574 + const patternRect = pattern.getBoundingClientRect(); 575 + dots.forEach(dot => { 576 + const dotX = patternRect.left + dot.x + 5; 577 + const dotY = patternRect.top + dot.y + 5; 578 + const dist = Math.hypot(mouseX - dotX, mouseY - dotY); 579 + const maxDist = 120; 580 + const scale = Math.min(1, Math.max(0.1, dist / maxDist)); 581 + dot.el.style.transform = `scale(${scale})`; 582 + }); 583 + requestAnimationFrame(updateDots); 584 + } 585 + updateDots(); 586 + 587 + const track = document.querySelector('.carousel-track'); 588 + const prevBtn = document.querySelector('.carousel-prev'); 589 + const nextBtn = document.querySelector('.carousel-next'); 590 + const slideWidth = track?.querySelector('.carousel-slide')?.offsetWidth + 16; 591 + 592 + prevBtn?.addEventListener('click', () => { 593 + track.scrollBy({ left: -slideWidth, behavior: 'smooth' }); 594 + }); 595 + nextBtn?.addEventListener('click', () => { 596 + track.scrollBy({ left: slideWidth, behavior: 'smooth' }); 597 + }); 598 + 599 + let isDragging = false; 600 + let startX, scrollLeft; 601 + 602 + track?.addEventListener('mousedown', e => { 603 + isDragging = true; 604 + track.style.cursor = 'grabbing'; 605 + track.style.scrollSnapType = 'none'; 606 + startX = e.pageX - track.offsetLeft; 607 + scrollLeft = track.scrollLeft; 608 + }); 609 + 610 + track?.addEventListener('mouseleave', () => { 611 + isDragging = false; 612 + track.style.cursor = 'grab'; 613 + track.style.scrollSnapType = 'x mandatory'; 614 + }); 615 + 616 + function snapTo(target, duration = 120) { 617 + const start = track.scrollLeft; 618 + const distance = target - start; 619 + const startTime = performance.now(); 620 + function step(currentTime) { 621 + const elapsed = currentTime - startTime; 622 + const progress = Math.min(elapsed / duration, 1); 623 + const ease = 1 - Math.pow(1 - progress, 3); 624 + track.scrollLeft = start + distance * ease; 625 + if (progress < 1) requestAnimationFrame(step); 626 + else track.style.scrollSnapType = 'x mandatory'; 627 + } 628 + requestAnimationFrame(step); 629 + } 630 + 631 + track?.addEventListener('mouseup', () => { 632 + isDragging = false; 633 + track.style.cursor = 'grab'; 634 + const slideW = track.querySelector('.carousel-slide').offsetWidth + 16; 635 + const targetIndex = Math.round(track.scrollLeft / slideW); 636 + snapTo(targetIndex * slideW); 637 + }); 638 + 639 + track?.addEventListener('mousemove', e => { 640 + if (!isDragging) return; 641 + e.preventDefault(); 642 + const x = e.pageX - track.offsetLeft; 643 + const walk = (x - startX) * 1.5; 644 + track.scrollLeft = scrollLeft - walk; 645 + }); 646 + 647 + if (track) track.style.cursor = 'grab'; 648 + </script> 649 + </body> 650 + </html>
+679
frontend/mockups/02-normal-colors.html
··· 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</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com"> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 + <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 10 + <style> 11 + * { margin: 0; padding: 0; box-sizing: border-box; } 12 + 13 + :root { 14 + --primary: #2c00ff; 15 + --primary-dark: #1a00a3; 16 + --primary-light: #4d33ff; 17 + --primary-muted: #e8e5ff; 18 + --secondary: #ff2400; 19 + --secondary-hover: #ff5533; 20 + --bg: #ffffff; 21 + --bg-subtle: #f8f8fa; 22 + --text: #1a1a1a; 23 + --text-muted: #666666; 24 + --text-light: #999999; 25 + --border: #e5e5e5; 26 + --border-light: #f0f0f0; 27 + } 28 + 29 + body { 30 + font-family: 'JetBrains Mono', monospace; 31 + line-height: 1.7; 32 + background: var(--bg); 33 + color: var(--text); 34 + min-height: 100vh; 35 + position: relative; 36 + } 37 + 38 + .pattern-container { 39 + position: fixed; 40 + top: -32px; 41 + left: -32px; 42 + right: -32px; 43 + bottom: -32px; 44 + pointer-events: none; 45 + z-index: 1; 46 + overflow: hidden; 47 + } 48 + 49 + .pattern { 50 + position: absolute; 51 + top: 0; 52 + left: 0; 53 + width: calc(100% + 500px); 54 + height: 100%; 55 + animation: drift 80s linear infinite; 56 + } 57 + 58 + .dot { 59 + position: absolute; 60 + width: 10px; 61 + height: 10px; 62 + background: rgba(0, 0, 0, 0.06); 63 + border-radius: 50%; 64 + transition: transform 0.04s linear; 65 + } 66 + 67 + .pattern-fade { 68 + position: fixed; 69 + top: 0; 70 + left: 0; 71 + right: 0; 72 + bottom: 0; 73 + background: linear-gradient(135deg, transparent 50%, var(--bg) 75%); 74 + pointer-events: none; 75 + z-index: 2; 76 + } 77 + 78 + @keyframes drift { 79 + 0% { transform: translateX(-500px); } 80 + 100% { transform: translateX(0); } 81 + } 82 + 83 + nav { z-index: 100; } 84 + main { position: relative; z-index: 10; } 85 + .site-footer { position: relative; z-index: 10; } 86 + 87 + a { color: var(--secondary); text-decoration: none; } 88 + a:hover { color: var(--secondary-hover); } 89 + 90 + nav { 91 + position: fixed; 92 + top: 12px; 93 + left: 32px; 94 + right: 32px; 95 + background: var(--primary); 96 + padding: 10px 18px; 97 + z-index: 100; 98 + border-radius: 8px; 99 + border: 1px solid rgba(0, 0, 0, 0.1); 100 + display: flex; 101 + justify-content: space-between; 102 + align-items: center; 103 + } 104 + 105 + nav .brand { 106 + font-weight: 600; 107 + font-size: 1rem; 108 + letter-spacing: 0.08em; 109 + color: #ffffff; 110 + text-transform: uppercase; 111 + } 112 + 113 + nav .nav-meta { 114 + font-size: 0.85rem; 115 + color: rgba(255, 255, 255, 0.7); 116 + letter-spacing: 0.05em; 117 + } 118 + 119 + main { 120 + max-width: 1000px; 121 + margin: 0 auto; 122 + padding: 100px 32px 80px; 123 + } 124 + 125 + .meta { 126 + display: flex; 127 + align-items: center; 128 + gap: 16px; 129 + margin-bottom: 32px; 130 + font-size: 0.8rem; 131 + font-weight: 500; 132 + text-transform: uppercase; 133 + letter-spacing: 0.1em; 134 + } 135 + 136 + .category { 137 + color: #ffffff; 138 + background: var(--primary); 139 + padding: 4px 10px; 140 + border-radius: 4px; 141 + } 142 + 143 + .read-time { 144 + color: var(--text-muted); 145 + } 146 + 147 + h1 { 148 + font-size: 2.75rem; 149 + font-weight: 600; 150 + line-height: 1.15; 151 + color: var(--text); 152 + margin-bottom: 32px; 153 + letter-spacing: -0.02em; 154 + } 155 + 156 + .byline { 157 + display: flex; 158 + align-items: center; 159 + gap: 16px; 160 + padding: 24px 0; 161 + border-top: 1px solid var(--border); 162 + border-bottom: 1px solid var(--border); 163 + margin-bottom: 48px; 164 + } 165 + 166 + .avatar { 167 + width: 44px; 168 + height: 44px; 169 + border-radius: 50%; 170 + background: linear-gradient(135deg, var(--secondary) 0%, #ff6b4a 100%); 171 + } 172 + 173 + .author-info { 174 + flex: 1; 175 + } 176 + 177 + .author { 178 + display: block; 179 + font-weight: 500; 180 + color: var(--text); 181 + font-size: 1rem; 182 + } 183 + 184 + .author-handle { 185 + display: block; 186 + font-size: 0.85rem; 187 + color: var(--text-muted); 188 + margin-top: 2px; 189 + } 190 + 191 + .verification { 192 + font-size: 0.75rem; 193 + font-weight: 500; 194 + color: var(--secondary); 195 + text-transform: uppercase; 196 + letter-spacing: 0.08em; 197 + } 198 + 199 + .placeholder-image { 200 + aspect-ratio: 16 / 9; 201 + background: var(--bg-subtle); 202 + border-radius: 8px; 203 + display: flex; 204 + align-items: center; 205 + justify-content: center; 206 + font-size: 0.9rem; 207 + color: var(--text-light); 208 + text-transform: uppercase; 209 + letter-spacing: 0.1em; 210 + border: 1px solid var(--border); 211 + } 212 + 213 + figcaption { 214 + margin-top: 12px; 215 + font-size: 0.85rem; 216 + color: var(--text-muted); 217 + text-align: center; 218 + } 219 + 220 + .carousel { 221 + margin: 64px 0 0; 222 + } 223 + 224 + .carousel-header { 225 + display: flex; 226 + justify-content: space-between; 227 + align-items: center; 228 + margin-bottom: 20px; 229 + } 230 + 231 + .carousel-title { 232 + font-size: 0.85rem; 233 + font-weight: 600; 234 + text-transform: uppercase; 235 + letter-spacing: 0.1em; 236 + color: var(--text); 237 + } 238 + 239 + .carousel-nav { 240 + display: flex; 241 + gap: 8px; 242 + } 243 + 244 + .carousel-nav button { 245 + font-family: 'JetBrains Mono', monospace; 246 + width: 36px; 247 + height: 36px; 248 + background: var(--bg); 249 + border: 1px solid var(--border); 250 + border-radius: 6px; 251 + color: var(--text); 252 + cursor: pointer; 253 + transition: all 0.15s ease; 254 + font-size: 1rem; 255 + } 256 + 257 + .carousel-nav button:hover { 258 + background: rgba(255, 36, 0, 0.08); 259 + border-color: var(--secondary); 260 + color: var(--secondary); 261 + } 262 + 263 + .carousel-track { 264 + display: flex; 265 + gap: 16px; 266 + overflow-x: auto; 267 + scroll-snap-type: x mandatory; 268 + scrollbar-width: none; 269 + -ms-overflow-style: none; 270 + padding-bottom: 8px; 271 + -webkit-overflow-scrolling: touch; 272 + user-select: none; 273 + } 274 + 275 + .carousel-track::-webkit-scrollbar { 276 + display: none; 277 + } 278 + 279 + .carousel-slide { 280 + flex: 0 0 70%; 281 + scroll-snap-align: start; 282 + } 283 + 284 + .carousel-slide .placeholder-image { 285 + aspect-ratio: 16 / 10; 286 + } 287 + 288 + .carousel-label { 289 + margin-top: 12px; 290 + font-size: 0.8rem; 291 + font-weight: 500; 292 + color: var(--text-muted); 293 + text-transform: uppercase; 294 + letter-spacing: 0.08em; 295 + } 296 + 297 + .content { 298 + font-size: 1.05rem; 299 + font-weight: 400; 300 + } 301 + 302 + .content p { 303 + margin-bottom: 28px; 304 + } 305 + 306 + .lede { 307 + font-size: 1.3rem; 308 + font-weight: 500; 309 + color: var(--text); 310 + line-height: 1.5; 311 + } 312 + 313 + .content h2 { 314 + font-size: 0.9rem; 315 + font-weight: 600; 316 + text-transform: uppercase; 317 + letter-spacing: 0.1em; 318 + color: var(--primary-dark); 319 + margin: 56px 0 24px; 320 + } 321 + 322 + blockquote { 323 + margin: 40px 0; 324 + padding: 32px; 325 + background: var(--primary-muted); 326 + border-left: 3px solid var(--primary); 327 + border-radius: 0 8px 8px 0; 328 + } 329 + 330 + blockquote p { 331 + font-size: 1.15rem; 332 + color: var(--primary-dark); 333 + font-style: italic; 334 + margin-bottom: 16px !important; 335 + } 336 + 337 + blockquote cite { 338 + font-size: 0.8rem; 339 + color: var(--text-muted); 340 + font-style: normal; 341 + text-transform: uppercase; 342 + letter-spacing: 0.05em; 343 + } 344 + 345 + .context-panel { 346 + margin: 40px 0; 347 + padding: 24px; 348 + background: var(--bg-subtle); 349 + border-radius: 8px; 350 + border: 1px solid var(--border); 351 + } 352 + 353 + .context-panel h3 { 354 + font-size: 0.8rem; 355 + font-weight: 600; 356 + text-transform: uppercase; 357 + letter-spacing: 0.1em; 358 + color: var(--text); 359 + margin-bottom: 16px; 360 + } 361 + 362 + .context-panel ul { 363 + list-style: none; 364 + } 365 + 366 + .context-panel li { 367 + padding: 10px 0; 368 + border-bottom: 1px solid var(--border-light); 369 + } 370 + 371 + .context-panel li:last-child { 372 + border-bottom: none; 373 + } 374 + 375 + .context-panel a { 376 + font-size: 0.95rem; 377 + font-weight: 500; 378 + color: var(--secondary); 379 + text-decoration: none; 380 + transition: color 0.15s ease; 381 + } 382 + 383 + .context-panel a:hover { 384 + color: var(--secondary-hover); 385 + } 386 + 387 + .article-footer { 388 + margin-top: 64px; 389 + padding-top: 32px; 390 + border-top: 1px solid var(--border); 391 + } 392 + 393 + .actions { 394 + display: flex; 395 + gap: 12px; 396 + margin-bottom: 24px; 397 + } 398 + 399 + .actions button { 400 + font-family: 'JetBrains Mono', monospace; 401 + font-size: 0.85rem; 402 + font-weight: 500; 403 + text-transform: uppercase; 404 + letter-spacing: 0.06em; 405 + padding: 14px 24px; 406 + background: var(--bg); 407 + border: 1px solid var(--border); 408 + border-radius: 6px; 409 + color: var(--text); 410 + cursor: pointer; 411 + transition: all 0.15s ease; 412 + } 413 + 414 + .actions button:hover { 415 + background: rgba(255, 36, 0, 0.08); 416 + border-color: var(--secondary); 417 + color: var(--secondary); 418 + } 419 + 420 + .actions button:first-child { 421 + background: var(--secondary); 422 + border-color: var(--secondary); 423 + color: #ffffff; 424 + } 425 + 426 + .actions button:first-child:hover { 427 + background: #cc1d00; 428 + border-color: #cc1d00; 429 + } 430 + 431 + .attestation-info { 432 + display: flex; 433 + flex-wrap: wrap; 434 + gap: 24px; 435 + font-size: 0.8rem; 436 + color: var(--text-light); 437 + text-transform: uppercase; 438 + letter-spacing: 0.05em; 439 + } 440 + 441 + .site-footer { 442 + max-width: 1000px; 443 + margin: 0 auto; 444 + padding: 48px 32px; 445 + display: flex; 446 + justify-content: space-between; 447 + font-size: 0.8rem; 448 + color: var(--text-light); 449 + text-transform: uppercase; 450 + letter-spacing: 0.05em; 451 + border-top: 1px solid var(--border); 452 + } 453 + 454 + ::selection { 455 + background: rgba(255, 36, 0, 0.2); 456 + color: var(--text); 457 + } 458 + </style> 459 + </head> 460 + <body> 461 + 462 + <div class="pattern-container"> 463 + <div class="pattern"></div> 464 + </div> 465 + <div class="pattern-fade"></div> 466 + 467 + <nav> 468 + <span class="brand">Tranquil PDS</span> 469 + <span class="nav-meta">0.1.0</span> 470 + </nav> 471 + 472 + <main> 473 + <article> 474 + <div class="meta"> 475 + <span class="category">Landing page</span> 476 + <span class="read-time">1 min read</span> 477 + </div> 478 + 479 + <h1>Lorem Ipsum Dolor Sit Amet Consectetur</h1> 480 + 481 + <div class="byline"> 482 + <div class="avatar"></div> 483 + <div class="author-info"> 484 + <span class="author">Mysterious benefactor</span> 485 + <span class="author-handle">@lewis.moe</span> 486 + </div> 487 + <div class="verification">47 attestations</div> 488 + </div> 489 + 490 + <div class="content"> 491 + <blockquote> 492 + <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p> 493 + <cite>Cicero, De Finibus Bonorum et Malorum</cite> 494 + </blockquote> 495 + 496 + <p class="lede">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.</p> 497 + 498 + <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p> 499 + 500 + <p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p> 501 + 502 + <h2>Neque Porro Quisquam</h2> 503 + 504 + <p>Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p> 505 + 506 + <p>Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.</p> 507 + 508 + <h2>Quis Autem Vel Eum</h2> 509 + 510 + <p>Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur.</p> 511 + 512 + <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.</p> 513 + 514 + <p>Similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.</p> 515 + 516 + <p>Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.</p> 517 + 518 + <p>Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae.</p> 519 + 520 + <div class="carousel"> 521 + <div class="carousel-header"> 522 + <span class="carousel-title">Interface</span> 523 + <div class="carousel-nav"> 524 + <button class="carousel-prev">←</button> 525 + <button class="carousel-next">→</button> 526 + </div> 527 + </div> 528 + <div class="carousel-track"> 529 + <div class="carousel-slide"> 530 + <div class="placeholder-image">Dashboard goes here</div> 531 + <div class="carousel-label">Dashboard</div> 532 + </div> 533 + <div class="carousel-slide"> 534 + <div class="placeholder-image">Profile Settings go here</div> 535 + <div class="carousel-label">Profile Settings</div> 536 + </div> 537 + <div class="carousel-slide"> 538 + <div class="placeholder-image">Account Security goes here</div> 539 + <div class="carousel-label">Account Security</div> 540 + </div> 541 + <div class="carousel-slide"> 542 + <div class="placeholder-image">Repository Browser goes here</div> 543 + <div class="carousel-label">Repository Browser</div> 544 + </div> 545 + <div class="carousel-slide"> 546 + <div class="placeholder-image">OAuth Applications goes here</div> 547 + <div class="carousel-label">OAuth Applications</div> 548 + </div> 549 + <div class="carousel-slide"> 550 + <div class="placeholder-image">Invite Codes goes here</div> 551 + <div class="carousel-label">Invite Codes</div> 552 + </div> 553 + </div> 554 + </div> 555 + </div> 556 + 557 + <footer class="article-footer"> 558 + <div class="actions"> 559 + <button>Propagate</button> 560 + <button>Annotate</button> 561 + <button>Verify Source</button> 562 + </div> 563 + 564 + <div class="attestation-info"> 565 + <span>hash: 7f3a9c...</span> 566 + <span>signed: 2847.12.03</span> 567 + <span>nodes: 12,847</span> 568 + </div> 569 + </footer> 570 + </article> 571 + </main> 572 + 573 + <footer class="site-footer"> 574 + <div>Mesh Commons License</div> 575 + <div>node: local-7f3a</div> 576 + </footer> 577 + 578 + <script> 579 + const pattern = document.querySelector('.pattern'); 580 + const spacing = 32; 581 + const cols = Math.ceil((window.innerWidth + 600) / spacing); 582 + const rows = Math.ceil((window.innerHeight + 100) / spacing); 583 + const dots = []; 584 + 585 + for (let y = 0; y < rows; y++) { 586 + for (let x = 0; x < cols; x++) { 587 + const dot = document.createElement('div'); 588 + dot.className = 'dot'; 589 + dot.style.left = (x * spacing) + 'px'; 590 + dot.style.top = (y * spacing) + 'px'; 591 + pattern.appendChild(dot); 592 + dots.push({ el: dot, x: x * spacing, y: y * spacing }); 593 + } 594 + } 595 + 596 + let mouseX = -1000, mouseY = -1000; 597 + document.addEventListener('mousemove', e => { 598 + mouseX = e.clientX; 599 + mouseY = e.clientY; 600 + }); 601 + 602 + function updateDots() { 603 + const patternRect = pattern.getBoundingClientRect(); 604 + dots.forEach(dot => { 605 + const dotX = patternRect.left + dot.x + 5; 606 + const dotY = patternRect.top + dot.y + 5; 607 + const dist = Math.hypot(mouseX - dotX, mouseY - dotY); 608 + const maxDist = 120; 609 + const scale = Math.min(1, Math.max(0.1, dist / maxDist)); 610 + dot.el.style.transform = `scale(${scale})`; 611 + }); 612 + requestAnimationFrame(updateDots); 613 + } 614 + updateDots(); 615 + 616 + const track = document.querySelector('.carousel-track'); 617 + const prevBtn = document.querySelector('.carousel-prev'); 618 + const nextBtn = document.querySelector('.carousel-next'); 619 + const slideWidth = track?.querySelector('.carousel-slide')?.offsetWidth + 16; 620 + 621 + prevBtn?.addEventListener('click', () => { 622 + track.scrollBy({ left: -slideWidth, behavior: 'smooth' }); 623 + }); 624 + nextBtn?.addEventListener('click', () => { 625 + track.scrollBy({ left: slideWidth, behavior: 'smooth' }); 626 + }); 627 + 628 + let isDragging = false; 629 + let startX, scrollLeft; 630 + 631 + track?.addEventListener('mousedown', e => { 632 + isDragging = true; 633 + track.style.cursor = 'grabbing'; 634 + track.style.scrollSnapType = 'none'; 635 + startX = e.pageX - track.offsetLeft; 636 + scrollLeft = track.scrollLeft; 637 + }); 638 + 639 + track?.addEventListener('mouseleave', () => { 640 + isDragging = false; 641 + track.style.cursor = 'grab'; 642 + track.style.scrollSnapType = 'x mandatory'; 643 + }); 644 + 645 + function snapTo(target, duration = 120) { 646 + const start = track.scrollLeft; 647 + const distance = target - start; 648 + const startTime = performance.now(); 649 + function step(currentTime) { 650 + const elapsed = currentTime - startTime; 651 + const progress = Math.min(elapsed / duration, 1); 652 + const ease = 1 - Math.pow(1 - progress, 3); 653 + track.scrollLeft = start + distance * ease; 654 + if (progress < 1) requestAnimationFrame(step); 655 + else track.style.scrollSnapType = 'x mandatory'; 656 + } 657 + requestAnimationFrame(step); 658 + } 659 + 660 + track?.addEventListener('mouseup', () => { 661 + isDragging = false; 662 + track.style.cursor = 'grab'; 663 + const slideW = track.querySelector('.carousel-slide').offsetWidth + 16; 664 + const targetIndex = Math.round(track.scrollLeft / slideW); 665 + snapTo(targetIndex * slideW); 666 + }); 667 + 668 + track?.addEventListener('mousemove', e => { 669 + if (!isDragging) return; 670 + e.preventDefault(); 671 + const x = e.pageX - track.offsetLeft; 672 + const walk = (x - startX) * 1.5; 673 + track.scrollLeft = scrollLeft - walk; 674 + }); 675 + 676 + if (track) track.style.cursor = 'grab'; 677 + </script> 678 + </body> 679 + </html>
+714
frontend/mockups/03-landing-page.html
··· 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</title> 7 + <link rel="preconnect" href="https://fonts.googleapis.com"> 8 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9 + <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 10 + <style> 11 + * { margin: 0; padding: 0; box-sizing: border-box; } 12 + 13 + :root { 14 + --primary: #2c00ff; 15 + --primary-dark: #1a00a3; 16 + --primary-light: #4d33ff; 17 + --primary-muted: #e8e5ff; 18 + --secondary: #ff2400; 19 + --secondary-hover: #ff5533; 20 + --bg: #ffffff; 21 + --bg-subtle: #f8f8fa; 22 + --text: #1a1a1a; 23 + --text-muted: #666666; 24 + --text-light: #999999; 25 + --border: #e5e5e5; 26 + --border-light: #f0f0f0; 27 + } 28 + 29 + body { 30 + font-family: 'JetBrains Mono', monospace; 31 + line-height: 1.7; 32 + background: var(--bg); 33 + color: var(--text); 34 + min-height: 100vh; 35 + position: relative; 36 + } 37 + 38 + .pattern-container { 39 + position: fixed; 40 + top: -32px; 41 + left: -32px; 42 + right: -32px; 43 + bottom: -32px; 44 + pointer-events: none; 45 + z-index: 1; 46 + overflow: hidden; 47 + } 48 + 49 + .pattern { 50 + position: absolute; 51 + top: 0; 52 + left: 0; 53 + width: calc(100% + 500px); 54 + height: 100%; 55 + animation: drift 80s linear infinite; 56 + } 57 + 58 + .dot { 59 + position: absolute; 60 + width: 10px; 61 + height: 10px; 62 + background: rgba(0, 0, 0, 0.06); 63 + border-radius: 50%; 64 + transition: transform 0.04s linear; 65 + } 66 + 67 + .pattern-fade { 68 + position: fixed; 69 + top: 0; 70 + left: 0; 71 + right: 0; 72 + bottom: 0; 73 + background: linear-gradient(135deg, transparent 50%, var(--bg) 75%); 74 + pointer-events: none; 75 + z-index: 2; 76 + } 77 + 78 + @keyframes drift { 79 + 0% { transform: translateX(-500px); } 80 + 100% { transform: translateX(0); } 81 + } 82 + 83 + nav { z-index: 100; } 84 + main { position: relative; z-index: 10; } 85 + .site-footer { position: relative; z-index: 10; } 86 + 87 + a { color: var(--secondary); text-decoration: none; } 88 + a:hover { color: var(--secondary-hover); } 89 + 90 + nav { 91 + position: fixed; 92 + top: 12px; 93 + left: 32px; 94 + right: 32px; 95 + background: var(--primary); 96 + padding: 10px 18px; 97 + z-index: 100; 98 + border-radius: 8px; 99 + border: 1px solid rgba(0, 0, 0, 0.1); 100 + display: flex; 101 + justify-content: space-between; 102 + align-items: center; 103 + } 104 + 105 + nav .brand { 106 + font-weight: 600; 107 + font-size: 1rem; 108 + letter-spacing: 0.08em; 109 + color: #ffffff; 110 + text-transform: uppercase; 111 + } 112 + 113 + nav .nav-meta { 114 + font-size: 0.85rem; 115 + color: rgba(255, 255, 255, 0.7); 116 + letter-spacing: 0.05em; 117 + } 118 + 119 + main { 120 + max-width: 1000px; 121 + margin: 0 auto; 122 + padding: 72px 32px 80px; 123 + } 124 + 125 + .meta { 126 + display: flex; 127 + align-items: center; 128 + gap: 16px; 129 + margin-bottom: 32px; 130 + font-size: 0.8rem; 131 + font-weight: 500; 132 + text-transform: uppercase; 133 + letter-spacing: 0.1em; 134 + } 135 + 136 + .category { 137 + color: #ffffff; 138 + background: var(--primary); 139 + padding: 4px 10px; 140 + border-radius: 4px; 141 + } 142 + 143 + .read-time { 144 + color: var(--text-muted); 145 + } 146 + 147 + h1 { 148 + font-size: 2.75rem; 149 + font-weight: 600; 150 + line-height: 1.15; 151 + color: var(--text); 152 + margin-bottom: 32px; 153 + letter-spacing: -0.02em; 154 + } 155 + 156 + .byline { 157 + display: flex; 158 + align-items: center; 159 + gap: 16px; 160 + padding: 24px 0; 161 + border-top: 1px solid var(--border); 162 + border-bottom: 1px solid var(--border); 163 + margin-bottom: 48px; 164 + } 165 + 166 + .avatar { 167 + width: 44px; 168 + height: 44px; 169 + border-radius: 50%; 170 + background: linear-gradient(135deg, var(--secondary) 0%, #ff6b4a 100%); 171 + } 172 + 173 + .author-info { 174 + flex: 1; 175 + } 176 + 177 + .author { 178 + display: block; 179 + font-weight: 500; 180 + color: var(--text); 181 + font-size: 1rem; 182 + } 183 + 184 + .author-handle { 185 + display: block; 186 + font-size: 0.85rem; 187 + color: var(--text-muted); 188 + margin-top: 2px; 189 + } 190 + 191 + .verification { 192 + font-size: 0.75rem; 193 + font-weight: 500; 194 + color: var(--secondary); 195 + text-transform: uppercase; 196 + letter-spacing: 0.08em; 197 + } 198 + 199 + .placeholder-image { 200 + aspect-ratio: 16 / 9; 201 + background: var(--bg-subtle); 202 + border-radius: 8px; 203 + display: flex; 204 + align-items: center; 205 + justify-content: center; 206 + font-size: 0.9rem; 207 + color: var(--text-light); 208 + text-transform: uppercase; 209 + letter-spacing: 0.1em; 210 + border: 1px solid var(--border); 211 + } 212 + 213 + figcaption { 214 + margin-top: 12px; 215 + font-size: 0.85rem; 216 + color: var(--text-muted); 217 + text-align: center; 218 + } 219 + 220 + .carousel { 221 + margin: 64px 0 0; 222 + } 223 + 224 + .carousel-header { 225 + display: flex; 226 + justify-content: space-between; 227 + align-items: center; 228 + margin-bottom: 20px; 229 + } 230 + 231 + .carousel-title { 232 + font-size: 0.85rem; 233 + font-weight: 600; 234 + text-transform: uppercase; 235 + letter-spacing: 0.1em; 236 + color: var(--text); 237 + } 238 + 239 + .carousel-nav { 240 + display: flex; 241 + gap: 8px; 242 + } 243 + 244 + .carousel-nav button { 245 + font-family: 'JetBrains Mono', monospace; 246 + width: 36px; 247 + height: 36px; 248 + background: var(--bg); 249 + border: 1px solid var(--border); 250 + border-radius: 6px; 251 + color: var(--text); 252 + cursor: pointer; 253 + transition: all 0.15s ease; 254 + font-size: 1rem; 255 + } 256 + 257 + .carousel-nav button:hover { 258 + background: rgba(255, 36, 0, 0.08); 259 + border-color: var(--secondary); 260 + color: var(--secondary); 261 + } 262 + 263 + .carousel-track { 264 + display: flex; 265 + gap: 16px; 266 + overflow-x: auto; 267 + scroll-snap-type: x mandatory; 268 + scrollbar-width: none; 269 + -ms-overflow-style: none; 270 + padding-bottom: 8px; 271 + -webkit-overflow-scrolling: touch; 272 + user-select: none; 273 + } 274 + 275 + .carousel-track::-webkit-scrollbar { 276 + display: none; 277 + } 278 + 279 + .carousel-slide { 280 + flex: 0 0 70%; 281 + scroll-snap-align: start; 282 + } 283 + 284 + .carousel-slide .placeholder-image { 285 + aspect-ratio: 16 / 10; 286 + } 287 + 288 + .carousel-label { 289 + margin-top: 12px; 290 + font-size: 0.8rem; 291 + font-weight: 500; 292 + color: var(--text-muted); 293 + text-transform: uppercase; 294 + letter-spacing: 0.08em; 295 + } 296 + 297 + .content { 298 + font-size: 1.05rem; 299 + font-weight: 400; 300 + } 301 + 302 + .content p { 303 + margin-bottom: 28px; 304 + } 305 + 306 + .lede { 307 + font-size: 1.3rem; 308 + font-weight: 500; 309 + color: var(--text); 310 + line-height: 1.5; 311 + } 312 + 313 + .hero { 314 + padding: 32px 0 40px; 315 + border-bottom: 1px solid var(--border); 316 + margin-bottom: 40px; 317 + } 318 + 319 + .content h2 { 320 + font-size: 0.9rem; 321 + font-weight: 600; 322 + text-transform: uppercase; 323 + letter-spacing: 0.1em; 324 + color: var(--primary-dark); 325 + margin: 56px 0 24px; 326 + } 327 + 328 + .content h2:first-child { 329 + margin-top: 0; 330 + } 331 + 332 + .features { 333 + display: grid; 334 + grid-template-columns: repeat(2, 1fr); 335 + gap: 32px; 336 + margin: 32px 0 56px; 337 + } 338 + 339 + .feature { 340 + padding: 24px; 341 + background: var(--bg-subtle); 342 + border-radius: 8px; 343 + border: 1px solid var(--border); 344 + } 345 + 346 + .feature h3 { 347 + font-size: 1rem; 348 + font-weight: 600; 349 + color: var(--text); 350 + margin-bottom: 12px; 351 + } 352 + 353 + .feature p { 354 + font-size: 0.95rem; 355 + color: var(--text-muted); 356 + margin-bottom: 0; 357 + line-height: 1.6; 358 + } 359 + 360 + @media (max-width: 700px) { 361 + .features { 362 + grid-template-columns: 1fr; 363 + } 364 + } 365 + 366 + blockquote { 367 + margin: 40px 0; 368 + padding: 32px; 369 + background: var(--primary-muted); 370 + border-left: 3px solid var(--primary); 371 + border-radius: 0 8px 8px 0; 372 + } 373 + 374 + blockquote p { 375 + font-size: 1.15rem; 376 + color: var(--primary-dark); 377 + font-style: italic; 378 + margin-bottom: 16px !important; 379 + } 380 + 381 + blockquote cite { 382 + font-size: 0.8rem; 383 + color: var(--text-muted); 384 + font-style: normal; 385 + text-transform: uppercase; 386 + letter-spacing: 0.05em; 387 + } 388 + 389 + .context-panel { 390 + margin: 40px 0; 391 + padding: 24px; 392 + background: var(--bg-subtle); 393 + border-radius: 8px; 394 + border: 1px solid var(--border); 395 + } 396 + 397 + .context-panel h3 { 398 + font-size: 0.8rem; 399 + font-weight: 600; 400 + text-transform: uppercase; 401 + letter-spacing: 0.1em; 402 + color: var(--text); 403 + margin-bottom: 16px; 404 + } 405 + 406 + .context-panel ul { 407 + list-style: none; 408 + } 409 + 410 + .context-panel li { 411 + padding: 10px 0; 412 + border-bottom: 1px solid var(--border-light); 413 + } 414 + 415 + .context-panel li:last-child { 416 + border-bottom: none; 417 + } 418 + 419 + .context-panel a { 420 + font-size: 0.95rem; 421 + font-weight: 500; 422 + color: var(--secondary); 423 + text-decoration: none; 424 + transition: color 0.15s ease; 425 + } 426 + 427 + .context-panel a:hover { 428 + color: var(--secondary-hover); 429 + } 430 + 431 + .article-footer { 432 + margin-top: 64px; 433 + padding-top: 32px; 434 + border-top: 1px solid var(--border); 435 + } 436 + 437 + .actions { 438 + display: flex; 439 + gap: 12px; 440 + margin-bottom: 24px; 441 + } 442 + 443 + .actions button { 444 + font-family: 'JetBrains Mono', monospace; 445 + font-size: 0.85rem; 446 + font-weight: 500; 447 + text-transform: uppercase; 448 + letter-spacing: 0.06em; 449 + padding: 14px 24px; 450 + background: var(--bg); 451 + border: 1px solid var(--border); 452 + border-radius: 6px; 453 + color: var(--text); 454 + cursor: pointer; 455 + transition: all 0.15s ease; 456 + } 457 + 458 + .actions button:hover { 459 + background: rgba(255, 36, 0, 0.08); 460 + border-color: var(--secondary); 461 + color: var(--secondary); 462 + } 463 + 464 + .actions button:first-child { 465 + background: var(--secondary); 466 + border-color: var(--secondary); 467 + color: #ffffff; 468 + } 469 + 470 + .actions button:first-child:hover { 471 + background: #cc1d00; 472 + border-color: #cc1d00; 473 + } 474 + 475 + .attestation-info { 476 + display: flex; 477 + flex-wrap: wrap; 478 + gap: 24px; 479 + font-size: 0.8rem; 480 + color: var(--text-light); 481 + text-transform: uppercase; 482 + letter-spacing: 0.05em; 483 + } 484 + 485 + .site-footer { 486 + max-width: 1000px; 487 + margin: 0 auto; 488 + padding: 48px 32px; 489 + display: flex; 490 + justify-content: space-between; 491 + font-size: 0.8rem; 492 + color: var(--text-light); 493 + text-transform: uppercase; 494 + letter-spacing: 0.05em; 495 + border-top: 1px solid var(--border); 496 + } 497 + 498 + ::selection { 499 + background: rgba(255, 36, 0, 0.2); 500 + color: var(--text); 501 + } 502 + </style> 503 + </head> 504 + <body> 505 + 506 + <div class="pattern-container"> 507 + <div class="pattern"></div> 508 + </div> 509 + <div class="pattern-fade"></div> 510 + 511 + <nav> 512 + <span class="brand">Tranquil PDS</span> 513 + <span class="nav-meta">0.1.0</span> 514 + </nav> 515 + 516 + <main> 517 + <section class="hero"> 518 + <h1>A home for your ATProto account</h1> 519 + 520 + <p class="lede">Tranquil PDS is a Personal Data Server, the thing that stores your posts, profile, and keys. Bluesky runs one for you, but you can run your own.</p> 521 + 522 + <div class="actions" style="margin-top: 40px; margin-bottom: 0;"> 523 + <button>Join This Server</button> 524 + <button>Run Your Own</button> 525 + </div> 526 + <blockquote> 527 + <p>"Nature does not hurry, yet everything is accomplished."</p> 528 + <cite>Lao Tzu</cite> 529 + </blockquote> 530 + </section> 531 + 532 + <section class="content"> 533 + <h2>What you get</h2> 534 + 535 + <div class="features"> 536 + <div class="feature"> 537 + <h3>Real security</h3> 538 + <p>Sign in with passkeys, add two-factor authentication, set up backup codes, and mark devices you trust. Your account stays yours.</p> 539 + </div> 540 + 541 + <div class="feature"> 542 + <h3>Your own identity</h3> 543 + <p>Use your own domain as your handle, or get a subdomain on ours. Either way, your identity moves with you if you ever leave.</p> 544 + </div> 545 + 546 + <div class="feature"> 547 + <h3>Stay in the loop</h3> 548 + <p>Get important alerts where you actually see them: email, Discord, Telegram, or Signal.</p> 549 + </div> 550 + 551 + <div class="feature"> 552 + <h3>You decide what apps can do</h3> 553 + <p>When an app asks for access, you'll see exactly what it wants in plain language. Grant what makes sense, deny what doesn't.</p> 554 + </div> 555 + </div> 556 + 557 + <h2>Everything in one place</h2> 558 + 559 + <p>Manage your profile, security settings, connected apps, and more from a clean dashboard. No command line or 3rd party apps required.</p> 560 + 561 + <div class="carousel"> 562 + <div class="carousel-header"> 563 + <span class="carousel-title">Interface</span> 564 + <div class="carousel-nav"> 565 + <button class="carousel-prev">←</button> 566 + <button class="carousel-next">→</button> 567 + </div> 568 + </div> 569 + <div class="carousel-track"> 570 + <div class="carousel-slide"> 571 + <div class="placeholder-image">Dashboard</div> 572 + <div class="carousel-label">Dashboard</div> 573 + </div> 574 + <div class="carousel-slide"> 575 + <div class="placeholder-image">Profile Settings</div> 576 + <div class="carousel-label">Profile Settings</div> 577 + </div> 578 + <div class="carousel-slide"> 579 + <div class="placeholder-image">Account Security</div> 580 + <div class="carousel-label">Account Security</div> 581 + </div> 582 + <div class="carousel-slide"> 583 + <div class="placeholder-image">Connected Apps</div> 584 + <div class="carousel-label">Connected Apps</div> 585 + </div> 586 + <div class="carousel-slide"> 587 + <div class="placeholder-image">Invite Friends</div> 588 + <div class="carousel-label">Invite Friends</div> 589 + </div> 590 + </div> 591 + </div> 592 + 593 + <h2>Works with everything</h2> 594 + 595 + <p>Use any ATProto app you already like. Tranquil PDS speaks the same language as Bluesky's servers, so all your favorite clients, tools, and bots just work.</p> 596 + 597 + <h2>Ready to try it?</h2> 598 + 599 + <p>Join this server, or grab the source and run your own. Either way, you can migrate an existing account over and your followers, posts, and identity come with you.</p> 600 + 601 + <div class="actions" style="margin-top: 32px;"> 602 + <button>Join This Server</button> 603 + <button>View Source</button> 604 + </div> 605 + </section> 606 + </main> 607 + 608 + <footer class="site-footer"> 609 + <div>Open Source</div> 610 + <div>Made with care</div> 611 + </footer> 612 + 613 + <script> 614 + const pattern = document.querySelector('.pattern'); 615 + const spacing = 32; 616 + const cols = Math.ceil((window.innerWidth + 600) / spacing); 617 + const rows = Math.ceil((window.innerHeight + 100) / spacing); 618 + const dots = []; 619 + 620 + for (let y = 0; y < rows; y++) { 621 + for (let x = 0; x < cols; x++) { 622 + const dot = document.createElement('div'); 623 + dot.className = 'dot'; 624 + dot.style.left = (x * spacing) + 'px'; 625 + dot.style.top = (y * spacing) + 'px'; 626 + pattern.appendChild(dot); 627 + dots.push({ el: dot, x: x * spacing, y: y * spacing }); 628 + } 629 + } 630 + 631 + let mouseX = -1000, mouseY = -1000; 632 + document.addEventListener('mousemove', e => { 633 + mouseX = e.clientX; 634 + mouseY = e.clientY; 635 + }); 636 + 637 + function updateDots() { 638 + const patternRect = pattern.getBoundingClientRect(); 639 + dots.forEach(dot => { 640 + const dotX = patternRect.left + dot.x + 5; 641 + const dotY = patternRect.top + dot.y + 5; 642 + const dist = Math.hypot(mouseX - dotX, mouseY - dotY); 643 + const maxDist = 120; 644 + const scale = Math.min(1, Math.max(0.1, dist / maxDist)); 645 + dot.el.style.transform = `scale(${scale})`; 646 + }); 647 + requestAnimationFrame(updateDots); 648 + } 649 + updateDots(); 650 + 651 + const track = document.querySelector('.carousel-track'); 652 + const prevBtn = document.querySelector('.carousel-prev'); 653 + const nextBtn = document.querySelector('.carousel-next'); 654 + const slideWidth = track?.querySelector('.carousel-slide')?.offsetWidth + 16; 655 + 656 + prevBtn?.addEventListener('click', () => { 657 + track.scrollBy({ left: -slideWidth, behavior: 'smooth' }); 658 + }); 659 + nextBtn?.addEventListener('click', () => { 660 + track.scrollBy({ left: slideWidth, behavior: 'smooth' }); 661 + }); 662 + 663 + let isDragging = false; 664 + let startX, scrollLeft; 665 + 666 + track?.addEventListener('mousedown', e => { 667 + isDragging = true; 668 + track.style.cursor = 'grabbing'; 669 + track.style.scrollSnapType = 'none'; 670 + startX = e.pageX - track.offsetLeft; 671 + scrollLeft = track.scrollLeft; 672 + }); 673 + 674 + track?.addEventListener('mouseleave', () => { 675 + isDragging = false; 676 + track.style.cursor = 'grab'; 677 + track.style.scrollSnapType = 'x mandatory'; 678 + }); 679 + 680 + function snapTo(target, duration = 120) { 681 + const start = track.scrollLeft; 682 + const distance = target - start; 683 + const startTime = performance.now(); 684 + function step(currentTime) { 685 + const elapsed = currentTime - startTime; 686 + const progress = Math.min(elapsed / duration, 1); 687 + const ease = 1 - Math.pow(1 - progress, 3); 688 + track.scrollLeft = start + distance * ease; 689 + if (progress < 1) requestAnimationFrame(step); 690 + else track.style.scrollSnapType = 'x mandatory'; 691 + } 692 + requestAnimationFrame(step); 693 + } 694 + 695 + track?.addEventListener('mouseup', () => { 696 + isDragging = false; 697 + track.style.cursor = 'grab'; 698 + const slideW = track.querySelector('.carousel-slide').offsetWidth + 16; 699 + const targetIndex = Math.round(track.scrollLeft / slideW); 700 + snapTo(targetIndex * slideW); 701 + }); 702 + 703 + track?.addEventListener('mousemove', e => { 704 + if (!isDragging) return; 705 + e.preventDefault(); 706 + const x = e.pageX - track.offsetLeft; 707 + const walk = (x - startX) * 1.5; 708 + track.scrollLeft = scrollLeft - walk; 709 + }); 710 + 711 + if (track) track.style.cursor = 'grab'; 712 + </script> 713 + </body> 714 + </html>
+15 -3
frontend/src/App.svelte
··· 1 1 <script lang="ts"> 2 - import { getCurrentPath } from './lib/router.svelte' 2 + import { getCurrentPath, navigate } from './lib/router.svelte' 3 3 import { initAuth, getAuthState } from './lib/auth.svelte' 4 4 import { initI18n, _ } from './lib/i18n' 5 5 import { isLoading as i18nLoading } from 'svelte-i18n' ··· 33 33 34 34 const auth = getAuthState() 35 35 36 + let oauthCallbackPending = $state(hasOAuthCallback()) 37 + 38 + function hasOAuthCallback(): boolean { 39 + const params = new URLSearchParams(window.location.search) 40 + return !!(params.get('code') && params.get('state')) 41 + } 42 + 36 43 $effect(() => { 37 - initAuth() 44 + initAuth().then(({ oauthLoginCompleted }) => { 45 + if (oauthLoginCompleted) { 46 + navigate('/dashboard') 47 + } 48 + oauthCallbackPending = false 49 + }) 38 50 }) 39 51 40 52 function getComponent(path: string) { ··· 97 109 </script> 98 110 99 111 <main> 100 - {#if auth.loading || $i18nLoading} 112 + {#if auth.loading || $i18nLoading || oauthCallbackPending} 101 113 <div class="loading"> 102 114 <p>Loading...</p> 103 115 </div>
+4 -3
frontend/src/lib/auth.svelte.ts
··· 111 111 } 112 112 } 113 113 114 - export async function initAuth() { 114 + export async function initAuth(): Promise<{ oauthLoginCompleted: boolean }> { 115 115 setTokenRefreshCallback(tryRefreshToken) 116 116 state.loading = true 117 117 state.error = null ··· 133 133 addOrUpdateSavedAccount(session) 134 134 applyLocaleFromSession(sessionInfo) 135 135 state.loading = false 136 - return 136 + return { oauthLoginCompleted: true } 137 137 } catch (e) { 138 138 state.error = e instanceof Error ? e.message : 'OAuth login failed' 139 139 state.loading = false 140 - return 140 + return { oauthLoginCompleted: false } 141 141 } 142 142 } 143 143 ··· 175 175 } 176 176 } 177 177 state.loading = false 178 + return { oauthLoginCompleted: false } 178 179 } 179 180 180 181 export async function login(identifier: string, password: string): Promise<void> {
+1
frontend/src/lib/router.svelte.ts
··· 10 10 }) 11 11 12 12 export function navigate(path: string) { 13 + currentPath = path 13 14 window.location.hash = path 14 15 } 15 16
+341 -90
frontend/src/routes/Home.svelte
··· 1 1 <script lang="ts"> 2 + import { onMount } from 'svelte' 2 3 import { _ } from '../lib/i18n' 3 4 import { getAuthState } from '../lib/auth.svelte' 5 + 4 6 const auth = getAuthState() 7 + const sourceUrl = 'https://tangled.org/lewis.moe/bspds-sandbox' 8 + 9 + onMount(() => { 10 + const pattern = document.getElementById('dotPattern') 11 + if (!pattern) return 12 + 13 + const spacing = 32 14 + const cols = Math.ceil((window.innerWidth + 600) / spacing) 15 + const rows = Math.ceil((window.innerHeight + 100) / spacing) 16 + const dots: { el: HTMLElement; x: number; y: number }[] = [] 17 + 18 + for (let y = 0; y < rows; y++) { 19 + for (let x = 0; x < cols; x++) { 20 + const dot = document.createElement('div') 21 + dot.className = 'dot' 22 + dot.style.left = (x * spacing) + 'px' 23 + dot.style.top = (y * spacing) + 'px' 24 + pattern.appendChild(dot) 25 + dots.push({ el: dot, x: x * spacing, y: y * spacing }) 26 + } 27 + } 28 + 29 + let mouseX = -1000 30 + let mouseY = -1000 31 + 32 + const handleMouseMove = (e: MouseEvent) => { 33 + mouseX = e.clientX 34 + mouseY = e.clientY 35 + } 36 + 37 + document.addEventListener('mousemove', handleMouseMove) 38 + 39 + let animationId: number 40 + 41 + function updateDots() { 42 + const patternRect = pattern.getBoundingClientRect() 43 + dots.forEach(dot => { 44 + const dotX = patternRect.left + dot.x + 5 45 + const dotY = patternRect.top + dot.y + 5 46 + const dist = Math.hypot(mouseX - dotX, mouseY - dotY) 47 + const maxDist = 120 48 + const scale = Math.min(1, Math.max(0.1, dist / maxDist)) 49 + dot.el.style.transform = `scale(${scale})` 50 + }) 51 + animationId = requestAnimationFrame(updateDots) 52 + } 53 + updateDots() 54 + 55 + return () => { 56 + document.removeEventListener('mousemove', handleMouseMove) 57 + cancelAnimationFrame(animationId) 58 + } 59 + }) 5 60 </script> 61 + 62 + <div class="pattern-container"> 63 + <div class="pattern" id="dotPattern"></div> 64 + </div> 65 + <div class="pattern-fade"></div> 66 + 67 + <nav> 68 + <span class="brand">Tranquil PDS</span> 69 + <span class="nav-meta">0.1.0</span> 70 + </nav> 71 + 6 72 <div class="home"> 7 - <header class="hero"> 8 - <h1>Tranquil PDS</h1> 9 - <p class="tagline">A Personal Data Server for the AT Protocol</p> 10 - </header> 11 - <section> 12 - <h2>What is a PDS?</h2> 13 - <p> 14 - Bluesky runs on a federated protocol called AT Protocol. Your account lives on a PDS, 15 - a server that stores your posts, profile, follows, and cryptographic keys. Bluesky hosts 16 - one for you at bsky.social, but you can run your own. Self-hosting means you control your 17 - data; you're not dependent on any company's servers, and your account + data is actually yours. 18 - </p> 73 + <section class="hero"> 74 + <h1>A home for your ATProto account</h1> 75 + 76 + <p class="lede">Tranquil PDS is a Personal Data Server, the thing that stores your posts, profile, and keys. Bluesky runs one for you, but you can run your own.</p> 77 + 78 + <div class="actions"> 79 + {#if auth.session} 80 + <a href="#/dashboard" class="btn primary">@{auth.session.handle}</a> 81 + {:else} 82 + <a href="#/register" class="btn primary">Join This Server</a> 83 + <a href={sourceUrl} class="btn secondary" target="_blank" rel="noopener">Run Your Own</a> 84 + {/if} 85 + </div> 86 + 87 + <blockquote> 88 + <p>"Nature does not hurry, yet everything is accomplished."</p> 89 + <cite>Lao Tzu</cite> 90 + </blockquote> 19 91 </section> 20 - <section> 21 - <h2>What's different about Tranquil?</h2> 22 - <p> 23 - This software isn't an afterthought by a company with limited resources. 24 - It is a superset of the reference PDS, including: 25 - </p> 26 - <ul> 27 - <li>Passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices)</li> 28 - <li>did:web support (PDS-hosted subdomains or bring-your-own)</li> 29 - <li>Multi-channel notifications (email, discord, telegram, signal)</li> 30 - <li>Granular OAuth scopes with a consent UI</li> 31 - <li>Built-in web UI for account management, repo browsing, and admin</li> 32 - </ul> 33 - <p> 34 - Full compatibility with Bluesky's reference PDS: same endpoints, same behavior, 35 - same client compatibility. Everything works. 36 - </p> 92 + 93 + <section class="content"> 94 + <h2>What you get</h2> 95 + 96 + <div class="features"> 97 + <div class="feature"> 98 + <h3>Real security</h3> 99 + <p>Sign in with passkeys, add two-factor authentication, set up backup codes, and mark devices you trust. Your account stays yours.</p> 100 + </div> 101 + 102 + <div class="feature"> 103 + <h3>Your own identity</h3> 104 + <p>Use your own domain as your handle, or get a subdomain on ours. Either way, your identity moves with you if you ever leave.</p> 105 + </div> 106 + 107 + <div class="feature"> 108 + <h3>Stay in the loop</h3> 109 + <p>Get important alerts where you actually see them: email, Discord, Telegram, or Signal.</p> 110 + </div> 111 + 112 + <div class="feature"> 113 + <h3>You decide what apps can do</h3> 114 + <p>When an app asks for access, you'll see exactly what it wants in plain language. Grant what makes sense, deny what doesn't.</p> 115 + </div> 116 + </div> 117 + 118 + <h2>Everything in one place</h2> 119 + 120 + <p>Manage your profile, security settings, connected apps, and more from a clean dashboard. No command line or 3rd party apps required.</p> 121 + 122 + <h2>Works with everything</h2> 123 + 124 + <p>Use any ATProto app you already like. Tranquil PDS speaks the same language as Bluesky's servers, so all your favorite clients, tools, and bots just work.</p> 125 + 126 + <h2>Ready to try it?</h2> 127 + 128 + <p>Join this server, or grab the source and run your own. Either way, you can migrate an existing account over and your followers, posts, and identity come with you.</p> 129 + 130 + <div class="actions"> 131 + {#if auth.session} 132 + <a href="#/dashboard" class="btn primary">@{auth.session.handle}</a> 133 + {:else} 134 + <a href="#/register" class="btn primary">Join This Server</a> 135 + <a href={sourceUrl} class="btn secondary" target="_blank" rel="noopener">View Source</a> 136 + {/if} 137 + </div> 37 138 </section> 38 - <div class="cta"> 39 - {#if auth.session} 40 - <a href="#/dashboard" class="btn">@{auth.session.handle}</a> 41 - {:else} 42 - <a href="#/login" class="btn">{$_('login.button')}</a> 43 - <a href="#/register" class="btn secondary">{$_('login.createAccount')}</a> 44 - {/if} 45 - </div> 46 - <footer> 47 - <a href="https://tangled.org/lewis.moe/bspds-sandbox" target="_blank" rel="noopener">Source code</a> 139 + 140 + <footer class="site-footer"> 141 + <span>Open Source</span> 142 + <span>Made with care</span> 48 143 </footer> 49 144 </div> 145 + 50 146 <style> 51 - .home { 52 - max-width: var(--width-md); 53 - margin: 0 auto; 54 - padding: var(--space-7); 147 + .pattern-container { 148 + position: fixed; 149 + top: -32px; 150 + left: -32px; 151 + right: -32px; 152 + bottom: -32px; 153 + pointer-events: none; 154 + z-index: 1; 155 + overflow: hidden; 156 + } 157 + 158 + .pattern { 159 + position: absolute; 160 + top: 0; 161 + left: 0; 162 + width: calc(100% + 500px); 163 + height: 100%; 164 + animation: drift 80s linear infinite; 165 + } 166 + 167 + .pattern :global(.dot) { 168 + position: absolute; 169 + width: 10px; 170 + height: 10px; 171 + background: rgba(0, 0, 0, 0.06); 172 + border-radius: 50%; 173 + transition: transform 0.04s linear; 174 + } 175 + 176 + @media (prefers-color-scheme: dark) { 177 + .pattern :global(.dot) { 178 + background: rgba(255, 255, 255, 0.1); 179 + } 180 + } 181 + 182 + .pattern-fade { 183 + position: fixed; 184 + top: 0; 185 + left: 0; 186 + right: 0; 187 + bottom: 0; 188 + background: linear-gradient(135deg, transparent 50%, var(--bg-primary) 75%); 189 + pointer-events: none; 190 + z-index: 2; 55 191 } 56 192 57 - .hero { 58 - text-align: center; 59 - margin-bottom: var(--space-8); 60 - padding-top: var(--space-7); 193 + @keyframes drift { 194 + 0% { transform: translateX(-500px); } 195 + 100% { transform: translateX(0); } 61 196 } 62 197 63 - .hero h1 { 64 - font-size: var(--text-4xl); 65 - margin-bottom: var(--space-3); 198 + nav { 199 + position: fixed; 200 + top: 12px; 201 + left: 32px; 202 + right: 32px; 203 + background: var(--accent); 204 + padding: 10px 18px; 205 + z-index: 100; 206 + border-radius: var(--radius-xl); 207 + display: flex; 208 + justify-content: space-between; 209 + align-items: center; 66 210 } 67 211 68 - .tagline { 69 - color: var(--text-secondary); 70 - font-size: var(--text-xl); 212 + .brand { 213 + font-weight: var(--font-semibold); 214 + font-size: var(--text-base); 215 + letter-spacing: 0.08em; 216 + color: var(--text-inverse); 217 + text-transform: uppercase; 71 218 } 72 219 73 - section { 74 - margin-bottom: var(--space-7); 220 + .nav-meta { 221 + font-size: var(--text-sm); 222 + color: rgba(255, 255, 255, 0.7); 223 + letter-spacing: 0.05em; 75 224 } 76 225 77 - h2 { 78 - margin-bottom: var(--space-4); 226 + .home { 227 + position: relative; 228 + z-index: 10; 229 + max-width: var(--width-xl); 230 + margin: 0 auto; 231 + padding: 72px 32px 32px; 79 232 } 80 233 81 - p { 82 - color: var(--text-secondary); 83 - margin-bottom: var(--space-4); 234 + .hero { 235 + padding: var(--space-7) 0 var(--space-8); 236 + border-bottom: 1px solid var(--border-color); 237 + margin-bottom: var(--space-8); 84 238 } 85 239 86 - ul { 87 - color: var(--text-secondary); 88 - margin: 0 0 var(--space-4) 0; 89 - padding-left: var(--space-6); 90 - line-height: var(--leading-relaxed); 240 + h1 { 241 + font-size: var(--text-4xl); 242 + font-weight: var(--font-semibold); 243 + line-height: var(--leading-tight); 244 + margin-bottom: var(--space-6); 245 + letter-spacing: -0.02em; 91 246 } 92 247 93 - li { 94 - margin-bottom: var(--space-2); 248 + .lede { 249 + font-size: var(--text-xl); 250 + font-weight: var(--font-medium); 251 + color: var(--text-primary); 252 + line-height: var(--leading-relaxed); 253 + margin-bottom: 0; 95 254 } 96 255 97 - .cta { 256 + .actions { 98 257 display: flex; 99 258 gap: var(--space-4); 100 - justify-content: center; 101 - margin: var(--space-8) 0; 259 + margin-top: var(--space-7); 102 260 } 103 261 104 262 .btn { 105 - display: inline-block; 106 - padding: var(--space-4) var(--space-7); 107 - border-radius: var(--radius-md); 108 - font-size: var(--text-base); 263 + font-size: var(--text-sm); 109 264 font-weight: var(--font-medium); 265 + text-transform: uppercase; 266 + letter-spacing: 0.06em; 267 + padding: var(--space-4) var(--space-6); 268 + border-radius: var(--radius-lg); 110 269 text-decoration: none; 111 - transition: background var(--transition-normal), border-color var(--transition-normal); 112 - background: var(--accent); 270 + transition: all var(--transition-normal); 271 + border: 1px solid transparent; 272 + } 273 + 274 + .btn.primary { 275 + background: var(--secondary); 113 276 color: var(--text-inverse); 277 + border-color: var(--secondary); 114 278 } 115 279 116 - .btn:hover { 117 - background: var(--accent-hover); 118 - text-decoration: none; 280 + .btn.primary:hover { 281 + background: var(--secondary-hover); 282 + border-color: var(--secondary-hover); 119 283 } 120 284 121 285 .btn.secondary { 122 286 background: transparent; 123 - color: var(--accent); 124 - border: 1px solid var(--accent); 287 + color: var(--text-primary); 288 + border-color: var(--border-color); 125 289 } 126 290 127 291 .btn.secondary:hover { 128 - background: var(--accent); 129 - color: var(--text-inverse); 292 + background: var(--secondary-muted); 293 + border-color: var(--secondary); 294 + color: var(--secondary); 295 + } 296 + 297 + blockquote { 298 + margin: var(--space-8) 0 0 0; 299 + padding: var(--space-6); 300 + background: var(--accent-muted); 301 + border-left: 3px solid var(--accent); 302 + border-radius: 0 var(--radius-xl) var(--radius-xl) 0; 130 303 } 131 304 132 - footer { 133 - text-align: center; 134 - padding-top: var(--space-7); 135 - border-top: 1px solid var(--border-color); 305 + blockquote p { 306 + font-size: var(--text-lg); 307 + color: var(--text-primary); 308 + font-style: italic; 309 + margin-bottom: var(--space-3); 136 310 } 137 311 138 - footer a { 139 - color: var(--text-muted); 312 + blockquote cite { 140 313 font-size: var(--text-sm); 314 + color: var(--text-secondary); 315 + font-style: normal; 316 + text-transform: uppercase; 317 + letter-spacing: 0.05em; 141 318 } 142 319 143 - footer a:hover { 320 + .content h2 { 321 + font-size: var(--text-sm); 322 + font-weight: var(--font-semibold); 323 + text-transform: uppercase; 324 + letter-spacing: 0.1em; 144 325 color: var(--accent); 326 + margin: var(--space-8) 0 var(--space-5); 327 + } 328 + 329 + .content h2:first-child { 330 + margin-top: 0; 331 + } 332 + 333 + .content > p { 334 + font-size: var(--text-base); 335 + color: var(--text-secondary); 336 + margin-bottom: var(--space-5); 337 + line-height: var(--leading-relaxed); 338 + } 339 + 340 + .features { 341 + display: grid; 342 + grid-template-columns: repeat(2, 1fr); 343 + gap: var(--space-6); 344 + margin: var(--space-6) 0 var(--space-8); 345 + } 346 + 347 + .feature { 348 + padding: var(--space-5); 349 + background: var(--bg-secondary); 350 + border-radius: var(--radius-xl); 351 + border: 1px solid var(--border-color); 352 + } 353 + 354 + .feature h3 { 355 + font-size: var(--text-base); 356 + font-weight: var(--font-semibold); 357 + color: var(--text-primary); 358 + margin-bottom: var(--space-3); 359 + } 360 + 361 + .feature p { 362 + font-size: var(--text-sm); 363 + color: var(--text-secondary); 364 + margin: 0; 365 + line-height: var(--leading-relaxed); 366 + } 367 + 368 + @media (max-width: 700px) { 369 + .features { 370 + grid-template-columns: 1fr; 371 + } 372 + 373 + h1 { 374 + font-size: var(--text-3xl); 375 + } 376 + 377 + .actions { 378 + flex-direction: column; 379 + } 380 + 381 + .btn { 382 + text-align: center; 383 + } 384 + } 385 + 386 + .site-footer { 387 + margin-top: var(--space-9); 388 + padding-top: var(--space-7); 389 + display: flex; 390 + justify-content: space-between; 391 + font-size: var(--text-sm); 392 + color: var(--text-muted); 393 + text-transform: uppercase; 394 + letter-spacing: 0.05em; 395 + border-top: 1px solid var(--border-color); 145 396 } 146 397 </style>
+11 -6
frontend/src/styles/base.css
··· 8 8 9 9 body { 10 10 margin: 0; 11 - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 11 + font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Monaco, monospace; 12 12 font-size: var(--text-base); 13 13 line-height: var(--leading-normal); 14 14 color: var(--text-primary); ··· 32 32 } 33 33 34 34 a { 35 - color: var(--accent); 35 + color: var(--secondary); 36 36 text-decoration: none; 37 37 } 38 38 39 39 a:hover { 40 - text-decoration: underline; 40 + color: var(--secondary-hover); 41 + text-decoration: none; 42 + } 43 + 44 + ::selection { 45 + background: var(--secondary-muted); 41 46 } 42 47 43 48 input, ··· 171 176 } 172 177 173 178 code { 174 - font-family: ui-monospace, 'SF Mono', Menlo, Monaco, 'Cascadia Code', monospace; 179 + font-family: inherit; 175 180 font-size: 0.9em; 176 181 background: var(--bg-tertiary); 177 182 padding: var(--space-1) var(--space-2); ··· 179 184 } 180 185 181 186 pre { 182 - font-family: ui-monospace, 'SF Mono', Menlo, Monaco, 'Cascadia Code', monospace; 187 + font-family: inherit; 183 188 font-size: var(--text-sm); 184 189 background: var(--bg-tertiary); 185 190 padding: var(--space-4); ··· 338 343 } 339 344 340 345 .mono { 341 - font-family: ui-monospace, 'SF Mono', Menlo, Monaco, 'Cascadia Code', monospace; 346 + font-family: inherit; 342 347 } 343 348 344 349 .mt-4 { margin-top: var(--space-4); }
+35 -25
frontend/src/styles/tokens.css
··· 48 48 --transition-normal: 0.15s ease; 49 49 --transition-slow: 0.25s ease; 50 50 51 - --bg-primary: #fafafa; 52 - --bg-secondary: #f5f5f5; 53 - --bg-tertiary: #eeeeee; 51 + --bg-primary: #ffffff; 52 + --bg-secondary: #f8f8fa; 53 + --bg-tertiary: #f0f0f2; 54 54 --bg-card: #ffffff; 55 55 --bg-input: #ffffff; 56 - --bg-input-disabled: #f5f5f5; 56 + --bg-input-disabled: #f8f8fa; 57 57 58 - --text-primary: #333333; 58 + --text-primary: #1a1a1a; 59 59 --text-secondary: #666666; 60 60 --text-muted: #999999; 61 61 --text-inverse: #ffffff; 62 62 63 - --border-color: #dddddd; 64 - --border-light: #eeeeee; 63 + --border-color: #e5e5e5; 64 + --border-light: #f0f0f0; 65 65 --border-dark: #cccccc; 66 66 67 - --accent: #0066cc; 68 - --accent-hover: #0052a3; 69 - --accent-muted: rgba(0, 102, 204, 0.15); 67 + --accent: #2c00ff; 68 + --accent-hover: #1a00a3; 69 + --accent-muted: rgba(44, 0, 255, 0.08); 70 + --accent-light: #4d33ff; 71 + 72 + --secondary: #ff2400; 73 + --secondary-hover: #cc1d00; 74 + --secondary-muted: rgba(255, 36, 0, 0.08); 70 75 71 76 --success-bg: #dfd; 72 77 --success-border: #8c8; ··· 85 90 86 91 @media (prefers-color-scheme: dark) { 87 92 :root { 88 - --bg-primary: #1a1a1a; 89 - --bg-secondary: #222222; 90 - --bg-tertiary: #2a2a2a; 91 - --bg-card: #2a2a2a; 92 - --bg-input: #333333; 93 - --bg-input-disabled: #2a2a2a; 93 + --bg-primary: #0a0a0a; 94 + --bg-secondary: #141414; 95 + --bg-tertiary: #1a1a1a; 96 + --bg-card: #141414; 97 + --bg-input: #1a1a1a; 98 + --bg-input-disabled: #141414; 94 99 95 - --text-primary: #e0e0e0; 100 + --text-primary: #e8e8e8; 96 101 --text-secondary: #a0a0a0; 97 - --text-muted: #707070; 98 - --text-inverse: #1a1a1a; 102 + --text-muted: #666666; 103 + --text-inverse: #0a0a0a; 99 104 100 - --border-color: #404040; 101 - --border-light: #333333; 102 - --border-dark: #505050; 105 + --border-color: #2a2a2a; 106 + --border-light: #222222; 107 + --border-dark: #333333; 108 + 109 + --accent: #2c00ff; 110 + --accent-hover: #4d33ff; 111 + --accent-muted: rgba(44, 0, 255, 0.15); 112 + --accent-light: #4d33ff; 103 113 104 - --accent: #4da6ff; 105 - --accent-hover: #7abbff; 106 - --accent-muted: rgba(77, 166, 255, 0.2); 114 + --secondary: #ff2400; 115 + --secondary-hover: #ff5533; 116 + --secondary-muted: rgba(255, 36, 0, 0.15); 107 117 108 118 --success-bg: #1a3d1a; 109 119 --success-border: #2d5a2d;
+8 -8
justfile
··· 45 45 DATABASE_URL="postgres://postgres:postgres@localhost:5432/pds" sqlx database drop -y 46 46 DATABASE_URL="postgres://postgres:postgres@localhost:5432/pds" sqlx database create 47 47 DATABASE_URL="postgres://postgres:postgres@localhost:5432/pds" sqlx migrate run 48 - docker-up: 49 - docker compose up -d 50 - docker-down: 51 - docker compose down 52 - docker-logs: 53 - docker compose logs -f 54 - docker-build: 55 - docker compose build 48 + podman-up: 49 + podman compose up -d 50 + podman-down: 51 + podman compose down 52 + podman-logs: 53 + podman compose logs -f 54 + podman-build: 55 + podman compose build 56 56 # Frontend commands (Deno) 57 57 frontend-dev: 58 58 . ~/.deno/env && cd frontend && deno task dev
+3 -14
src/oauth/client.rs
··· 88 88 89 89 fn is_loopback_client(client_id: &str) -> bool { 90 90 if let Ok(url) = reqwest::Url::parse(client_id) { 91 - url.scheme() == "http" && url.host_str() == Some("localhost") && url.port().is_none() 91 + url.scheme() == "http" 92 + && matches!(url.host_str(), Some("localhost") | Some("127.0.0.1")) 92 93 } else { 93 94 false 94 95 } ··· 310 311 let is_loopback_redirect = req_url.scheme() == "http" 311 312 && (req_host == "localhost" || req_host == "127.0.0.1" || req_host == "[::1]"); 312 313 if is_loopback_redirect { 313 - for registered in &metadata.redirect_uris { 314 - if let Ok(reg_url) = reqwest::Url::parse(registered) { 315 - let reg_host = reg_url.host_str().unwrap_or(""); 316 - let hosts_match = (req_host == "localhost" && reg_host == "localhost") 317 - || (req_host == "127.0.0.1" && reg_host == "127.0.0.1") 318 - || (req_host == "[::1]" && reg_host == "[::1]") 319 - || (req_host == "localhost" && reg_host == "127.0.0.1") 320 - || (req_host == "127.0.0.1" && reg_host == "localhost"); 321 - if hosts_match && req_url.path() == reg_url.path() { 322 - return Ok(()); 323 - } 324 - } 325 - } 314 + return Ok(()); 326 315 } 327 316 } 328 317 Err(OAuthError::InvalidRequest(