this repo has no description
1<script lang="ts"> 2 import type { OutboundMigrationFlow } from '../../lib/migration' 3 import type { ServerDescription } from '../../lib/migration/types' 4 import { getAuthState, logout } from '../../lib/auth.svelte' 5 6 interface Props { 7 flow: OutboundMigrationFlow 8 onBack: () => void 9 onComplete: () => void 10 } 11 12 let { flow, onBack, onComplete }: Props = $props() 13 14 const auth = getAuthState() 15 16 let loading = $state(false) 17 let understood = $state(false) 18 let pdsUrlInput = $state('') 19 let handleInput = $state('') 20 let selectedDomain = $state('') 21 let confirmFinal = $state(false) 22 23 $effect(() => { 24 if (flow.state.step === 'success') { 25 setTimeout(async () => { 26 await logout() 27 onComplete() 28 }, 3000) 29 } 30 }) 31 32 $effect(() => { 33 if (flow.state.targetServerInfo?.availableUserDomains?.length) { 34 selectedDomain = flow.state.targetServerInfo.availableUserDomains[0] 35 } 36 }) 37 38 async function validatePds(e: Event) { 39 e.preventDefault() 40 loading = true 41 flow.updateField('error', null) 42 43 try { 44 let url = pdsUrlInput.trim() 45 if (!url.startsWith('http://') && !url.startsWith('https://')) { 46 url = `https://${url}` 47 } 48 await flow.validateTargetPds(url) 49 flow.setStep('new-account') 50 } catch (err) { 51 flow.setError((err as Error).message) 52 } finally { 53 loading = false 54 } 55 } 56 57 function proceedToReview() { 58 const fullHandle = handleInput.includes('.') 59 ? handleInput 60 : `${handleInput}.${selectedDomain}` 61 62 flow.updateField('targetHandle', fullHandle) 63 flow.setStep('review') 64 } 65 66 async function startMigration() { 67 if (!auth.session) return 68 loading = true 69 try { 70 await flow.startMigration(auth.session.did) 71 } catch (err) { 72 flow.setError((err as Error).message) 73 } finally { 74 loading = false 75 } 76 } 77 78 async function submitPlcToken(e: Event) { 79 e.preventDefault() 80 loading = true 81 try { 82 await flow.submitPlcToken(flow.state.plcToken) 83 } catch (err) { 84 flow.setError((err as Error).message) 85 } finally { 86 loading = false 87 } 88 } 89 90 async function resendToken() { 91 loading = true 92 try { 93 await flow.resendPlcToken() 94 flow.setError(null) 95 } catch (err) { 96 flow.setError((err as Error).message) 97 } finally { 98 loading = false 99 } 100 } 101 102 function isDidWeb(): boolean { 103 return auth.session?.did?.startsWith('did:web:') ?? false 104 } 105 106 const steps = ['Target', 'Setup', 'Review', 'Transfer', 'Verify', 'Complete'] 107 function getCurrentStepIndex(): number { 108 switch (flow.state.step) { 109 case 'welcome': return -1 110 case 'target-pds': return 0 111 case 'new-account': return 1 112 case 'review': return 2 113 case 'migrating': return 3 114 case 'plc-token': 115 case 'finalizing': return 4 116 case 'success': return 5 117 default: return 0 118 } 119 } 120</script> 121 122<div class="outbound-wizard"> 123 {#if flow.state.step !== 'welcome'} 124 <div class="step-indicator"> 125 {#each steps as stepName, i} 126 <div class="step" class:active={i === getCurrentStepIndex()} class:completed={i < getCurrentStepIndex()}> 127 <div class="step-dot">{i < getCurrentStepIndex() ? '✓' : i + 1}</div> 128 <span class="step-label">{stepName}</span> 129 </div> 130 {#if i < steps.length - 1} 131 <div class="step-line" class:completed={i < getCurrentStepIndex()}></div> 132 {/if} 133 {/each} 134 </div> 135 {/if} 136 137 {#if flow.state.error} 138 <div class="message error">{flow.state.error}</div> 139 {/if} 140 141 {#if flow.state.step === 'welcome'} 142 <div class="step-content"> 143 <h2>Migrate Your Account Away</h2> 144 <p>This wizard will help you move your AT Protocol account from this PDS to another one.</p> 145 146 <div class="current-account"> 147 <span class="label">Current account:</span> 148 <span class="value">@{auth.session?.handle}</span> 149 </div> 150 151 {#if isDidWeb()} 152 <div class="warning-box"> 153 <strong>did:web Migration Notice</strong> 154 <p> 155 Your account uses a did:web identifier ({auth.session?.did}). After migrating, this PDS will 156 continue serving your DID document with an updated service endpoint pointing to your new PDS. 157 </p> 158 <p> 159 You can return here anytime to update the forwarding if you migrate again in the future. 160 </p> 161 </div> 162 {/if} 163 164 <div class="info-box"> 165 <h3>What will happen:</h3> 166 <ol> 167 <li>Choose your new PDS</li> 168 <li>Set up your account on the new server</li> 169 <li>Your repository and blobs will be transferred</li> 170 <li>Verify the migration via email</li> 171 <li>Your identity will be updated to point to the new PDS</li> 172 <li>Your account here will be deactivated</li> 173 </ol> 174 </div> 175 176 <div class="warning-box"> 177 <strong>Before you proceed:</strong> 178 <ul> 179 <li>You need access to the email registered with this account</li> 180 <li>You will lose access to this account on this PDS</li> 181 <li>Make sure you trust the destination PDS</li> 182 <li>Large accounts may take several minutes to transfer</li> 183 </ul> 184 </div> 185 186 <label class="checkbox-label"> 187 <input type="checkbox" bind:checked={understood} /> 188 <span>I understand that my account will be moved and deactivated here</span> 189 </label> 190 191 <div class="button-row"> 192 <button class="ghost" onclick={onBack}>Cancel</button> 193 <button disabled={!understood} onclick={() => flow.setStep('target-pds')}> 194 Continue 195 </button> 196 </div> 197 </div> 198 199 {:else if flow.state.step === 'target-pds'} 200 <div class="step-content"> 201 <h2>Choose Your New PDS</h2> 202 <p>Enter the URL of the PDS you want to migrate to.</p> 203 204 <form onsubmit={validatePds}> 205 <div class="field"> 206 <label for="pds-url">PDS URL</label> 207 <input 208 id="pds-url" 209 type="text" 210 placeholder="pds.example.com" 211 bind:value={pdsUrlInput} 212 disabled={loading} 213 required 214 /> 215 <p class="hint">The server address of your new PDS (e.g., bsky.social, pds.example.com)</p> 216 </div> 217 218 <div class="button-row"> 219 <button type="button" class="ghost" onclick={() => flow.setStep('welcome')} disabled={loading}>Back</button> 220 <button type="submit" disabled={loading || !pdsUrlInput.trim()}> 221 {loading ? 'Checking...' : 'Connect'} 222 </button> 223 </div> 224 </form> 225 226 {#if flow.state.targetServerInfo} 227 <div class="server-info"> 228 <h3>Connected to PDS</h3> 229 <div class="info-row"> 230 <span class="label">Server:</span> 231 <span class="value">{flow.state.targetPdsUrl}</span> 232 </div> 233 {#if flow.state.targetServerInfo.availableUserDomains.length > 0} 234 <div class="info-row"> 235 <span class="label">Available domains:</span> 236 <span class="value">{flow.state.targetServerInfo.availableUserDomains.join(', ')}</span> 237 </div> 238 {/if} 239 <div class="info-row"> 240 <span class="label">Invite required:</span> 241 <span class="value">{flow.state.targetServerInfo.inviteCodeRequired ? 'Yes' : 'No'}</span> 242 </div> 243 {#if flow.state.targetServerInfo.links?.termsOfService} 244 <a href={flow.state.targetServerInfo.links.termsOfService} target="_blank" rel="noopener"> 245 Terms of Service 246 </a> 247 {/if} 248 {#if flow.state.targetServerInfo.links?.privacyPolicy} 249 <a href={flow.state.targetServerInfo.links.privacyPolicy} target="_blank" rel="noopener"> 250 Privacy Policy 251 </a> 252 {/if} 253 </div> 254 {/if} 255 </div> 256 257 {:else if flow.state.step === 'new-account'} 258 <div class="step-content"> 259 <h2>Set Up Your New Account</h2> 260 <p>Configure your account details on the new PDS.</p> 261 262 <div class="current-info"> 263 <span class="label">Migrating to:</span> 264 <span class="value">{flow.state.targetPdsUrl}</span> 265 </div> 266 267 <div class="field"> 268 <label for="new-handle">New Handle</label> 269 <div class="handle-input-group"> 270 <input 271 id="new-handle" 272 type="text" 273 placeholder="username" 274 bind:value={handleInput} 275 /> 276 {#if flow.state.targetServerInfo && flow.state.targetServerInfo.availableUserDomains.length > 0 && !handleInput.includes('.')} 277 <select bind:value={selectedDomain}> 278 {#each flow.state.targetServerInfo.availableUserDomains as domain} 279 <option value={domain}>.{domain}</option> 280 {/each} 281 </select> 282 {/if} 283 </div> 284 <p class="hint">You can also use your own domain by entering the full handle (e.g., alice.mydomain.com)</p> 285 </div> 286 287 <div class="field"> 288 <label for="email">Email Address</label> 289 <input 290 id="email" 291 type="email" 292 placeholder="you@example.com" 293 bind:value={flow.state.targetEmail} 294 oninput={(e) => flow.updateField('targetEmail', (e.target as HTMLInputElement).value)} 295 required 296 /> 297 </div> 298 299 <div class="field"> 300 <label for="new-password">Password</label> 301 <input 302 id="new-password" 303 type="password" 304 placeholder="Password for your new account" 305 bind:value={flow.state.targetPassword} 306 oninput={(e) => flow.updateField('targetPassword', (e.target as HTMLInputElement).value)} 307 required 308 minlength="8" 309 /> 310 <p class="hint">At least 8 characters. This will be your password on the new PDS.</p> 311 </div> 312 313 {#if flow.state.targetServerInfo?.inviteCodeRequired} 314 <div class="field"> 315 <label for="invite">Invite Code</label> 316 <input 317 id="invite" 318 type="text" 319 placeholder="Enter invite code" 320 bind:value={flow.state.inviteCode} 321 oninput={(e) => flow.updateField('inviteCode', (e.target as HTMLInputElement).value)} 322 required 323 /> 324 <p class="hint">Required by this PDS to create an account</p> 325 </div> 326 {/if} 327 328 <div class="button-row"> 329 <button class="ghost" onclick={() => flow.setStep('target-pds')}>Back</button> 330 <button 331 disabled={!handleInput.trim() || !flow.state.targetEmail || !flow.state.targetPassword} 332 onclick={proceedToReview} 333 > 334 Continue 335 </button> 336 </div> 337 </div> 338 339 {:else if flow.state.step === 'review'} 340 <div class="step-content"> 341 <h2>Review Migration</h2> 342 <p>Please confirm the details of your migration.</p> 343 344 <div class="review-card"> 345 <div class="review-row"> 346 <span class="label">Current Handle:</span> 347 <span class="value">@{auth.session?.handle}</span> 348 </div> 349 <div class="review-row"> 350 <span class="label">New Handle:</span> 351 <span class="value">@{flow.state.targetHandle}</span> 352 </div> 353 <div class="review-row"> 354 <span class="label">DID:</span> 355 <span class="value mono">{auth.session?.did}</span> 356 </div> 357 <div class="review-row"> 358 <span class="label">From PDS:</span> 359 <span class="value">{window.location.origin}</span> 360 </div> 361 <div class="review-row"> 362 <span class="label">To PDS:</span> 363 <span class="value">{flow.state.targetPdsUrl}</span> 364 </div> 365 <div class="review-row"> 366 <span class="label">New Email:</span> 367 <span class="value">{flow.state.targetEmail}</span> 368 </div> 369 </div> 370 371 <div class="warning-box final-warning"> 372 <strong>This action cannot be easily undone!</strong> 373 <p> 374 After migration completes, your account on this PDS will be deactivated. 375 To return, you would need to migrate back from the new PDS. 376 </p> 377 </div> 378 379 <label class="checkbox-label"> 380 <input type="checkbox" bind:checked={confirmFinal} /> 381 <span>I confirm I want to migrate my account to {flow.state.targetPdsUrl}</span> 382 </label> 383 384 <div class="button-row"> 385 <button class="ghost" onclick={() => flow.setStep('new-account')} disabled={loading}>Back</button> 386 <button class="danger" onclick={startMigration} disabled={loading || !confirmFinal}> 387 {loading ? 'Starting...' : 'Start Migration'} 388 </button> 389 </div> 390 </div> 391 392 {:else if flow.state.step === 'migrating'} 393 <div class="step-content"> 394 <h2>Migration in Progress</h2> 395 <p>Please wait while your account is being transferred...</p> 396 397 <div class="progress-section"> 398 <div class="progress-item" class:completed={flow.state.progress.repoExported}> 399 <span class="icon">{flow.state.progress.repoExported ? '✓' : '○'}</span> 400 <span>Export repository</span> 401 </div> 402 <div class="progress-item" class:completed={flow.state.progress.repoImported}> 403 <span class="icon">{flow.state.progress.repoImported ? '✓' : '○'}</span> 404 <span>Import repository to new PDS</span> 405 </div> 406 <div class="progress-item" class:active={flow.state.progress.repoImported && !flow.state.progress.prefsMigrated}> 407 <span class="icon">{flow.state.progress.blobsMigrated === flow.state.progress.blobsTotal && flow.state.progress.blobsTotal > 0 ? '✓' : '○'}</span> 408 <span>Migrate blobs ({flow.state.progress.blobsMigrated}/{flow.state.progress.blobsTotal})</span> 409 </div> 410 <div class="progress-item" class:completed={flow.state.progress.prefsMigrated}> 411 <span class="icon">{flow.state.progress.prefsMigrated ? '✓' : '○'}</span> 412 <span>Migrate preferences</span> 413 </div> 414 </div> 415 416 {#if flow.state.progress.blobsTotal > 0} 417 <div class="progress-bar"> 418 <div 419 class="progress-fill" 420 style="width: {(flow.state.progress.blobsMigrated / flow.state.progress.blobsTotal) * 100}%" 421 ></div> 422 </div> 423 {/if} 424 425 <p class="status-text">{flow.state.progress.currentOperation}</p> 426 </div> 427 428 {:else if flow.state.step === 'plc-token'} 429 <div class="step-content"> 430 <h2>Verify Migration</h2> 431 <p>A verification code has been sent to your email ({auth.session?.email}).</p> 432 433 <div class="info-box"> 434 <p> 435 This code confirms you have access to the account and authorizes updating your identity 436 to point to the new PDS. 437 </p> 438 </div> 439 440 <form onsubmit={submitPlcToken}> 441 <div class="field"> 442 <label for="plc-token">Verification Code</label> 443 <input 444 id="plc-token" 445 type="text" 446 placeholder="Enter code from email" 447 bind:value={flow.state.plcToken} 448 oninput={(e) => flow.updateField('plcToken', (e.target as HTMLInputElement).value)} 449 disabled={loading} 450 required 451 /> 452 </div> 453 454 <div class="button-row"> 455 <button type="button" class="ghost" onclick={resendToken} disabled={loading}> 456 Resend Code 457 </button> 458 <button type="submit" disabled={loading || !flow.state.plcToken}> 459 {loading ? 'Verifying...' : 'Complete Migration'} 460 </button> 461 </div> 462 </form> 463 </div> 464 465 {:else if flow.state.step === 'finalizing'} 466 <div class="step-content"> 467 <h2>Finalizing Migration</h2> 468 <p>Please wait while we complete the migration...</p> 469 470 <div class="progress-section"> 471 <div class="progress-item" class:completed={flow.state.progress.plcSigned}> 472 <span class="icon">{flow.state.progress.plcSigned ? '✓' : '○'}</span> 473 <span>Sign identity update</span> 474 </div> 475 <div class="progress-item" class:completed={flow.state.progress.activated}> 476 <span class="icon">{flow.state.progress.activated ? '✓' : '○'}</span> 477 <span>Activate account on new PDS</span> 478 </div> 479 <div class="progress-item" class:completed={flow.state.progress.deactivated}> 480 <span class="icon">{flow.state.progress.deactivated ? '✓' : '○'}</span> 481 <span>Deactivate account here</span> 482 </div> 483 </div> 484 485 <p class="status-text">{flow.state.progress.currentOperation}</p> 486 </div> 487 488 {:else if flow.state.step === 'success'} 489 <div class="step-content success-content"> 490 <div class="success-icon"></div> 491 <h2>Migration Complete!</h2> 492 <p>Your account has been successfully migrated to your new PDS.</p> 493 494 <div class="success-details"> 495 <div class="detail-row"> 496 <span class="label">Your new handle:</span> 497 <span class="value">@{flow.state.targetHandle}</span> 498 </div> 499 <div class="detail-row"> 500 <span class="label">New PDS:</span> 501 <span class="value">{flow.state.targetPdsUrl}</span> 502 </div> 503 <div class="detail-row"> 504 <span class="label">DID:</span> 505 <span class="value mono">{auth.session?.did}</span> 506 </div> 507 </div> 508 509 {#if flow.state.progress.blobsFailed.length > 0} 510 <div class="warning-box"> 511 <strong>Note:</strong> {flow.state.progress.blobsFailed.length} blobs could not be migrated. 512 These may be images or other media that are no longer available. 513 </div> 514 {/if} 515 516 <div class="next-steps"> 517 <h3>Next Steps</h3> 518 <ol> 519 <li>Visit your new PDS at <a href={flow.state.targetPdsUrl} target="_blank" rel="noopener">{flow.state.targetPdsUrl}</a></li> 520 <li>Log in with your new credentials</li> 521 <li>Your followers and following will continue to work</li> 522 </ol> 523 </div> 524 525 <p class="redirect-text">Logging out in a moment...</p> 526 </div> 527 528 {:else if flow.state.step === 'error'} 529 <div class="step-content"> 530 <h2>Migration Error</h2> 531 <p>An error occurred during migration.</p> 532 533 <div class="error-box"> 534 {flow.state.error} 535 </div> 536 537 <div class="button-row"> 538 <button class="ghost" onclick={onBack}>Start Over</button> 539 </div> 540 </div> 541 {/if} 542</div> 543 544<style> 545 .outbound-wizard { 546 max-width: 600px; 547 margin: 0 auto; 548 } 549 550 .step-indicator { 551 display: flex; 552 align-items: center; 553 justify-content: center; 554 margin-bottom: var(--space-8); 555 padding: 0 var(--space-4); 556 } 557 558 .step { 559 display: flex; 560 flex-direction: column; 561 align-items: center; 562 gap: var(--space-2); 563 } 564 565 .step-dot { 566 width: 32px; 567 height: 32px; 568 border-radius: 50%; 569 background: var(--bg-secondary); 570 border: 2px solid var(--border); 571 display: flex; 572 align-items: center; 573 justify-content: center; 574 font-size: var(--text-sm); 575 font-weight: var(--font-medium); 576 color: var(--text-secondary); 577 } 578 579 .step.active .step-dot { 580 background: var(--accent); 581 border-color: var(--accent); 582 color: var(--text-inverse); 583 } 584 585 .step.completed .step-dot { 586 background: var(--success-bg); 587 border-color: var(--success-text); 588 color: var(--success-text); 589 } 590 591 .step-label { 592 font-size: var(--text-xs); 593 color: var(--text-secondary); 594 } 595 596 .step.active .step-label { 597 color: var(--accent); 598 font-weight: var(--font-medium); 599 } 600 601 .step-line { 602 flex: 1; 603 height: 2px; 604 background: var(--border); 605 margin: 0 var(--space-2); 606 margin-bottom: var(--space-6); 607 min-width: 20px; 608 } 609 610 .step-line.completed { 611 background: var(--success-text); 612 } 613 614 .step-content { 615 background: var(--bg-secondary); 616 border-radius: var(--radius-xl); 617 padding: var(--space-6); 618 } 619 620 .step-content h2 { 621 margin: 0 0 var(--space-3) 0; 622 } 623 624 .step-content > p { 625 color: var(--text-secondary); 626 margin: 0 0 var(--space-5) 0; 627 } 628 629 .current-account { 630 background: var(--bg-primary); 631 border-radius: var(--radius-lg); 632 padding: var(--space-4); 633 margin-bottom: var(--space-5); 634 display: flex; 635 justify-content: space-between; 636 align-items: center; 637 } 638 639 .current-account .label { 640 color: var(--text-secondary); 641 } 642 643 .current-account .value { 644 font-weight: var(--font-medium); 645 font-size: var(--text-lg); 646 } 647 648 .info-box { 649 background: var(--accent-muted); 650 border: 1px solid var(--accent); 651 border-radius: var(--radius-lg); 652 padding: var(--space-5); 653 margin-bottom: var(--space-5); 654 } 655 656 .info-box h3 { 657 margin: 0 0 var(--space-3) 0; 658 font-size: var(--text-base); 659 } 660 661 .info-box ol, .info-box ul { 662 margin: 0; 663 padding-left: var(--space-5); 664 } 665 666 .info-box li { 667 margin-bottom: var(--space-2); 668 color: var(--text-secondary); 669 } 670 671 .info-box p { 672 margin: 0; 673 color: var(--text-secondary); 674 } 675 676 .warning-box { 677 background: var(--warning-bg); 678 border: 1px solid var(--warning-border); 679 border-radius: var(--radius-lg); 680 padding: var(--space-5); 681 margin-bottom: var(--space-5); 682 font-size: var(--text-sm); 683 } 684 685 .warning-box strong { 686 color: var(--warning-text); 687 } 688 689 .warning-box p { 690 margin: var(--space-3) 0 0 0; 691 color: var(--text-secondary); 692 } 693 694 .warning-box ul { 695 margin: var(--space-3) 0 0 0; 696 padding-left: var(--space-5); 697 } 698 699 .final-warning { 700 background: var(--error-bg); 701 border-color: var(--error-border); 702 } 703 704 .final-warning strong { 705 color: var(--error-text); 706 } 707 708 .error-box { 709 background: var(--error-bg); 710 border: 1px solid var(--error-border); 711 border-radius: var(--radius-lg); 712 padding: var(--space-5); 713 margin-bottom: var(--space-5); 714 color: var(--error-text); 715 } 716 717 .checkbox-label { 718 display: inline-flex; 719 align-items: flex-start; 720 gap: var(--space-3); 721 cursor: pointer; 722 margin-bottom: var(--space-5); 723 text-align: left; 724 } 725 726 .checkbox-label input[type="checkbox"] { 727 width: 18px; 728 height: 18px; 729 margin: 0; 730 flex-shrink: 0; 731 } 732 733 .button-row { 734 display: flex; 735 gap: var(--space-3); 736 justify-content: flex-end; 737 margin-top: var(--space-5); 738 } 739 740 .field { 741 margin-bottom: var(--space-5); 742 } 743 744 .field label { 745 display: block; 746 margin-bottom: var(--space-2); 747 font-weight: var(--font-medium); 748 } 749 750 .field input, .field select { 751 width: 100%; 752 padding: var(--space-3); 753 border: 1px solid var(--border); 754 border-radius: var(--radius-md); 755 background: var(--bg-primary); 756 color: var(--text-primary); 757 } 758 759 .field input:focus, .field select:focus { 760 outline: none; 761 border-color: var(--accent); 762 } 763 764 .hint { 765 font-size: var(--text-sm); 766 color: var(--text-secondary); 767 margin: var(--space-2) 0 0 0; 768 } 769 770 .handle-input-group { 771 display: flex; 772 gap: var(--space-2); 773 } 774 775 .handle-input-group input { 776 flex: 1; 777 } 778 779 .handle-input-group select { 780 width: auto; 781 } 782 783 .current-info { 784 background: var(--bg-primary); 785 border-radius: var(--radius-lg); 786 padding: var(--space-4); 787 margin-bottom: var(--space-5); 788 display: flex; 789 justify-content: space-between; 790 } 791 792 .current-info .label { 793 color: var(--text-secondary); 794 } 795 796 .current-info .value { 797 font-weight: var(--font-medium); 798 } 799 800 .server-info { 801 background: var(--bg-primary); 802 border-radius: var(--radius-lg); 803 padding: var(--space-4); 804 margin-top: var(--space-5); 805 } 806 807 .server-info h3 { 808 margin: 0 0 var(--space-3) 0; 809 font-size: var(--text-base); 810 color: var(--success-text); 811 } 812 813 .server-info .info-row { 814 display: flex; 815 justify-content: space-between; 816 padding: var(--space-2) 0; 817 font-size: var(--text-sm); 818 } 819 820 .server-info .label { 821 color: var(--text-secondary); 822 } 823 824 .server-info a { 825 display: inline-block; 826 margin-top: var(--space-2); 827 margin-right: var(--space-3); 828 color: var(--accent); 829 font-size: var(--text-sm); 830 } 831 832 .review-card { 833 background: var(--bg-primary); 834 border-radius: var(--radius-lg); 835 padding: var(--space-4); 836 margin-bottom: var(--space-5); 837 } 838 839 .review-row { 840 display: flex; 841 justify-content: space-between; 842 padding: var(--space-3) 0; 843 border-bottom: 1px solid var(--border); 844 } 845 846 .review-row:last-child { 847 border-bottom: none; 848 } 849 850 .review-row .label { 851 color: var(--text-secondary); 852 } 853 854 .review-row .value { 855 font-weight: var(--font-medium); 856 text-align: right; 857 word-break: break-all; 858 } 859 860 .review-row .value.mono { 861 font-family: var(--font-mono); 862 font-size: var(--text-sm); 863 } 864 865 .progress-section { 866 margin-bottom: var(--space-5); 867 } 868 869 .progress-item { 870 display: flex; 871 align-items: center; 872 gap: var(--space-3); 873 padding: var(--space-3) 0; 874 color: var(--text-secondary); 875 } 876 877 .progress-item.completed { 878 color: var(--success-text); 879 } 880 881 .progress-item.active { 882 color: var(--accent); 883 } 884 885 .progress-item .icon { 886 width: 24px; 887 text-align: center; 888 } 889 890 .progress-bar { 891 height: 8px; 892 background: var(--bg-primary); 893 border-radius: 4px; 894 overflow: hidden; 895 margin-bottom: var(--space-4); 896 } 897 898 .progress-fill { 899 height: 100%; 900 background: var(--accent); 901 transition: width 0.3s ease; 902 } 903 904 .status-text { 905 text-align: center; 906 color: var(--text-secondary); 907 font-size: var(--text-sm); 908 } 909 910 .success-content { 911 text-align: center; 912 } 913 914 .success-icon { 915 width: 64px; 916 height: 64px; 917 background: var(--success-bg); 918 color: var(--success-text); 919 border-radius: 50%; 920 display: flex; 921 align-items: center; 922 justify-content: center; 923 font-size: var(--text-2xl); 924 margin: 0 auto var(--space-5) auto; 925 } 926 927 .success-details { 928 background: var(--bg-primary); 929 border-radius: var(--radius-lg); 930 padding: var(--space-4); 931 margin: var(--space-5) 0; 932 text-align: left; 933 } 934 935 .success-details .detail-row { 936 display: flex; 937 justify-content: space-between; 938 padding: var(--space-2) 0; 939 } 940 941 .success-details .label { 942 color: var(--text-secondary); 943 } 944 945 .success-details .value { 946 font-weight: var(--font-medium); 947 } 948 949 .success-details .value.mono { 950 font-family: var(--font-mono); 951 font-size: var(--text-sm); 952 } 953 954 .next-steps { 955 background: var(--accent-muted); 956 border-radius: var(--radius-lg); 957 padding: var(--space-5); 958 margin: var(--space-5) 0; 959 text-align: left; 960 } 961 962 .next-steps h3 { 963 margin: 0 0 var(--space-3) 0; 964 } 965 966 .next-steps ol { 967 margin: 0; 968 padding-left: var(--space-5); 969 } 970 971 .next-steps li { 972 margin-bottom: var(--space-2); 973 } 974 975 .next-steps a { 976 color: var(--accent); 977 } 978 979 .redirect-text { 980 color: var(--text-secondary); 981 font-style: italic; 982 } 983 984 .message.error { 985 background: var(--error-bg); 986 border: 1px solid var(--error-border); 987 color: var(--error-text); 988 padding: var(--space-4); 989 border-radius: var(--radius-lg); 990 margin-bottom: var(--space-5); 991 } 992</style>