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