this repo has no description
1<script lang="ts"> 2 import { onMount } from 'svelte' 3 import { _ } from '../lib/i18n' 4 import { getAuthState } from '../lib/auth.svelte' 5 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 }) 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 72<div class="home"> 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> 91 </section> 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> 138 </section> 139 140 <footer class="site-footer"> 141 <span>Open Source</span> 142 <span>Made with care</span> 143 </footer> 144</div> 145 146<style> 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; 191 } 192 193 @keyframes drift { 194 0% { transform: translateX(-500px); } 195 100% { transform: translateX(0); } 196 } 197 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; 210 } 211 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; 218 } 219 220 .nav-meta { 221 font-size: var(--text-sm); 222 color: rgba(255, 255, 255, 0.7); 223 letter-spacing: 0.05em; 224 } 225 226 .home { 227 position: relative; 228 z-index: 10; 229 max-width: var(--width-xl); 230 margin: 0 auto; 231 padding: 72px 32px 32px; 232 } 233 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); 238 } 239 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; 246 } 247 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; 254 } 255 256 .actions { 257 display: flex; 258 gap: var(--space-4); 259 margin-top: var(--space-7); 260 } 261 262 .btn { 263 font-size: var(--text-sm); 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); 269 text-decoration: none; 270 transition: all var(--transition-normal); 271 border: 1px solid transparent; 272 } 273 274 .btn.primary { 275 background: var(--secondary); 276 color: var(--text-inverse); 277 border-color: var(--secondary); 278 } 279 280 .btn.primary:hover { 281 background: var(--secondary-hover); 282 border-color: var(--secondary-hover); 283 } 284 285 .btn.secondary { 286 background: transparent; 287 color: var(--text-primary); 288 border-color: var(--border-color); 289 } 290 291 .btn.secondary:hover { 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; 303 } 304 305 blockquote p { 306 font-size: var(--text-lg); 307 color: var(--text-primary); 308 font-style: italic; 309 margin-bottom: var(--space-3); 310 } 311 312 blockquote cite { 313 font-size: var(--text-sm); 314 color: var(--text-secondary); 315 font-style: normal; 316 text-transform: uppercase; 317 letter-spacing: 0.05em; 318 } 319 320 .content h2 { 321 font-size: var(--text-sm); 322 font-weight: var(--font-semibold); 323 text-transform: uppercase; 324 letter-spacing: 0.1em; 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); 396 } 397</style>