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 "@context": [ 624 "https://www.w3.org/ns/did/v1", 625 "https://w3id.org/security/multikey/v1", 626 "https://w3id.org/security/suites/secp256k1-2019/v1" 627 ], 628 "id": "${flow.state.sourceDid}", 629 "alsoKnownAs": [ 630 "at://${flow.state.targetHandle || '...'}" 631 ], 632 "verificationMethod": [ 633 { 634 "id": "${flow.state.sourceDid}#atproto", 635 "type": "Multikey", 636 "controller": "${flow.state.sourceDid}", 637 "publicKeyMultibase": "${flow.state.targetVerificationMethod?.replace('did:key:', '') || '...'}" 638 } 639 ], 640 "service": [ 641 { 642 "id": "#atproto_pds", 643 "type": "AtprotoPersonalDataServer", 644 "serviceEndpoint": "${window.location.origin}" 645 } 646 ] 647}`}</pre> 648 </div> 649 650 <div class="warning-box"> 651 <strong>{$_('migration.inbound.didWebUpdate.important')}</strong> {$_('migration.inbound.didWebUpdate.verifyFirst')} 652 {$_('migration.inbound.didWebUpdate.fileLocation')} <code>https://{flow.state.sourceDid.replace('did:web:', '')}/.well-known/did.json</code> 653 </div> 654 655 <div class="button-row"> 656 <button class="ghost" onclick={() => flow.setStep('email-verify')} disabled={loading}>Back</button> 657 <button onclick={completeDidWeb} disabled={loading}> 658 {loading ? $_('migration.inbound.didWebUpdate.completing') : $_('migration.inbound.didWebUpdate.complete')} 659 </button> 660 </div> 661 </div> 662 663 {:else if flow.state.step === 'finalizing'} 664 <div class="step-content"> 665 <h2>Finalizing Migration</h2> 666 <p>Please wait while we complete the migration...</p> 667 668 <div class="progress-section"> 669 <div class="progress-item" class:completed={flow.state.progress.plcSigned}> 670 <span class="icon">{flow.state.progress.plcSigned ? '✓' : '○'}</span> 671 <span>Sign identity update</span> 672 </div> 673 <div class="progress-item" class:completed={flow.state.progress.activated}> 674 <span class="icon">{flow.state.progress.activated ? '✓' : '○'}</span> 675 <span>Activate new account</span> 676 </div> 677 <div class="progress-item" class:completed={flow.state.progress.deactivated}> 678 <span class="icon">{flow.state.progress.deactivated ? '✓' : '○'}</span> 679 <span>Deactivate old account</span> 680 </div> 681 </div> 682 683 <p class="status-text">{flow.state.progress.currentOperation}</p> 684 </div> 685 686 {:else if flow.state.step === 'success'} 687 <div class="step-content success-content"> 688 <div class="success-icon"></div> 689 <h2>Migration Complete!</h2> 690 <p>Your account has been successfully migrated to this PDS.</p> 691 692 <div class="success-details"> 693 <div class="detail-row"> 694 <span class="label">Your new handle:</span> 695 <span class="value">{flow.state.targetHandle}</span> 696 </div> 697 <div class="detail-row"> 698 <span class="label">DID:</span> 699 <span class="value mono">{flow.state.sourceDid}</span> 700 </div> 701 </div> 702 703 {#if flow.state.progress.blobsFailed.length > 0} 704 <div class="warning-box"> 705 <strong>Note:</strong> {flow.state.progress.blobsFailed.length} blobs could not be migrated. 706 These may be images or other media that are no longer available. 707 </div> 708 {/if} 709 710 <p class="redirect-text">Redirecting to dashboard...</p> 711 </div> 712 713 {:else if flow.state.step === 'error'} 714 <div class="step-content"> 715 <h2>Migration Error</h2> 716 <p>An error occurred during migration.</p> 717 718 <div class="error-box"> 719 {flow.state.error} 720 </div> 721 722 <div class="button-row"> 723 <button class="ghost" onclick={onBack}>Start Over</button> 724 </div> 725 </div> 726 {/if} 727</div> 728 729<style> 730 .inbound-wizard { 731 max-width: 600px; 732 margin: 0 auto; 733 } 734 735 .step-indicator { 736 display: flex; 737 align-items: center; 738 justify-content: center; 739 margin-bottom: var(--space-8); 740 padding: 0 var(--space-4); 741 } 742 743 .step { 744 display: flex; 745 flex-direction: column; 746 align-items: center; 747 gap: var(--space-2); 748 } 749 750 .step-dot { 751 width: 32px; 752 height: 32px; 753 border-radius: 50%; 754 background: var(--bg-secondary); 755 border: 2px solid var(--border); 756 display: flex; 757 align-items: center; 758 justify-content: center; 759 font-size: var(--text-sm); 760 font-weight: var(--font-medium); 761 color: var(--text-secondary); 762 } 763 764 .step.active .step-dot { 765 background: var(--accent); 766 border-color: var(--accent); 767 color: var(--text-inverse); 768 } 769 770 .step.completed .step-dot { 771 background: var(--success-bg); 772 border-color: var(--success-text); 773 color: var(--success-text); 774 } 775 776 .step-label { 777 font-size: var(--text-xs); 778 color: var(--text-secondary); 779 } 780 781 .step.active .step-label { 782 color: var(--accent); 783 font-weight: var(--font-medium); 784 } 785 786 .step-line { 787 flex: 1; 788 height: 2px; 789 background: var(--border); 790 margin: 0 var(--space-2); 791 margin-bottom: var(--space-6); 792 min-width: 20px; 793 } 794 795 .step-line.completed { 796 background: var(--success-text); 797 } 798 799 .step-content { 800 background: var(--bg-secondary); 801 border-radius: var(--radius-xl); 802 padding: var(--space-6); 803 } 804 805 .step-content h2 { 806 margin: 0 0 var(--space-3) 0; 807 } 808 809 .step-content > p { 810 color: var(--text-secondary); 811 margin: 0 0 var(--space-5) 0; 812 } 813 814 .info-box { 815 background: var(--accent-muted); 816 border: 1px solid var(--accent); 817 border-radius: var(--radius-lg); 818 padding: var(--space-5); 819 margin-bottom: var(--space-5); 820 } 821 822 .info-box h3 { 823 margin: 0 0 var(--space-3) 0; 824 font-size: var(--text-base); 825 } 826 827 .info-box ol, .info-box ul { 828 margin: 0; 829 padding-left: var(--space-5); 830 } 831 832 .info-box li { 833 margin-bottom: var(--space-2); 834 color: var(--text-secondary); 835 } 836 837 .info-box p { 838 margin: 0; 839 color: var(--text-secondary); 840 } 841 842 .warning-box { 843 background: var(--warning-bg); 844 border: 1px solid var(--warning-border); 845 border-radius: var(--radius-lg); 846 padding: var(--space-5); 847 margin-bottom: var(--space-5); 848 font-size: var(--text-sm); 849 } 850 851 .warning-box strong { 852 color: var(--warning-text); 853 } 854 855 .warning-box ul { 856 margin: var(--space-3) 0 0 0; 857 padding-left: var(--space-5); 858 } 859 860 .error-box { 861 background: var(--error-bg); 862 border: 1px solid var(--error-border); 863 border-radius: var(--radius-lg); 864 padding: var(--space-5); 865 margin-bottom: var(--space-5); 866 color: var(--error-text); 867 } 868 869 .checkbox-label { 870 display: inline-flex; 871 align-items: flex-start; 872 gap: var(--space-3); 873 cursor: pointer; 874 margin-bottom: var(--space-5); 875 text-align: left; 876 } 877 878 .checkbox-label input[type="checkbox"] { 879 width: 18px; 880 height: 18px; 881 margin: 0; 882 flex-shrink: 0; 883 } 884 885 .button-row { 886 display: flex; 887 gap: var(--space-3); 888 justify-content: flex-end; 889 margin-top: var(--space-5); 890 } 891 892 .field { 893 margin-bottom: var(--space-5); 894 } 895 896 .field label { 897 display: block; 898 margin-bottom: var(--space-2); 899 font-weight: var(--font-medium); 900 } 901 902 .field input, .field select { 903 width: 100%; 904 padding: var(--space-3); 905 border: 1px solid var(--border); 906 border-radius: var(--radius-md); 907 background: var(--bg-primary); 908 color: var(--text-primary); 909 } 910 911 .field input:focus, .field select:focus { 912 outline: none; 913 border-color: var(--accent); 914 } 915 916 .hint { 917 font-size: var(--text-sm); 918 color: var(--text-secondary); 919 margin: var(--space-2) 0 0 0; 920 } 921 922 .hint.success { 923 color: var(--success-text); 924 } 925 926 .hint.error { 927 color: var(--error-text); 928 } 929 930 .handle-input-group { 931 display: flex; 932 gap: var(--space-2); 933 } 934 935 .handle-input-group input { 936 flex: 1; 937 } 938 939 .handle-input-group select { 940 width: auto; 941 } 942 943 .current-info { 944 background: var(--bg-primary); 945 border-radius: var(--radius-lg); 946 padding: var(--space-4); 947 margin-bottom: var(--space-5); 948 display: flex; 949 justify-content: space-between; 950 } 951 952 .current-info .label { 953 color: var(--text-secondary); 954 } 955 956 .current-info .value { 957 font-weight: var(--font-medium); 958 } 959 960 .review-card { 961 background: var(--bg-primary); 962 border-radius: var(--radius-lg); 963 padding: var(--space-4); 964 margin-bottom: var(--space-5); 965 } 966 967 .review-row { 968 display: flex; 969 justify-content: space-between; 970 padding: var(--space-3) 0; 971 border-bottom: 1px solid var(--border); 972 } 973 974 .review-row:last-child { 975 border-bottom: none; 976 } 977 978 .review-row .label { 979 color: var(--text-secondary); 980 } 981 982 .review-row .value { 983 font-weight: var(--font-medium); 984 text-align: right; 985 word-break: break-all; 986 } 987 988 .review-row .value.mono { 989 font-family: var(--font-mono); 990 font-size: var(--text-sm); 991 } 992 993 .progress-section { 994 margin-bottom: var(--space-5); 995 } 996 997 .progress-item { 998 display: flex; 999 align-items: center; 1000 gap: var(--space-3); 1001 padding: var(--space-3) 0; 1002 color: var(--text-secondary); 1003 } 1004 1005 .progress-item.completed { 1006 color: var(--success-text); 1007 } 1008 1009 .progress-item.active { 1010 color: var(--accent); 1011 } 1012 1013 .progress-item .icon { 1014 width: 24px; 1015 text-align: center; 1016 } 1017 1018 .progress-bar { 1019 height: 8px; 1020 background: var(--bg-primary); 1021 border-radius: 4px; 1022 overflow: hidden; 1023 margin-bottom: var(--space-4); 1024 } 1025 1026 .progress-fill { 1027 height: 100%; 1028 background: var(--accent); 1029 transition: width 0.3s ease; 1030 } 1031 1032 .status-text { 1033 text-align: center; 1034 color: var(--text-secondary); 1035 font-size: var(--text-sm); 1036 } 1037 1038 .success-content { 1039 text-align: center; 1040 } 1041 1042 .success-icon { 1043 width: 64px; 1044 height: 64px; 1045 background: var(--success-bg); 1046 color: var(--success-text); 1047 border-radius: 50%; 1048 display: flex; 1049 align-items: center; 1050 justify-content: center; 1051 font-size: var(--text-2xl); 1052 margin: 0 auto var(--space-5) auto; 1053 } 1054 1055 .success-details { 1056 background: var(--bg-primary); 1057 border-radius: var(--radius-lg); 1058 padding: var(--space-4); 1059 margin: var(--space-5) 0; 1060 text-align: left; 1061 } 1062 1063 .success-details .detail-row { 1064 display: flex; 1065 justify-content: space-between; 1066 padding: var(--space-2) 0; 1067 } 1068 1069 .success-details .label { 1070 color: var(--text-secondary); 1071 } 1072 1073 .success-details .value { 1074 font-weight: var(--font-medium); 1075 } 1076 1077 .success-details .value.mono { 1078 font-family: var(--font-mono); 1079 font-size: var(--text-sm); 1080 } 1081 1082 .redirect-text { 1083 color: var(--text-secondary); 1084 font-style: italic; 1085 } 1086 1087 .message.error { 1088 background: var(--error-bg); 1089 border: 1px solid var(--error-border); 1090 color: var(--error-text); 1091 padding: var(--space-4); 1092 border-radius: var(--radius-lg); 1093 margin-bottom: var(--space-5); 1094 } 1095 1096 .code-block { 1097 background: var(--bg-primary); 1098 border: 1px solid var(--border); 1099 border-radius: var(--radius-lg); 1100 padding: var(--space-4); 1101 margin-bottom: var(--space-5); 1102 overflow-x: auto; 1103 } 1104 1105 .code-block pre { 1106 margin: 0; 1107 font-family: var(--font-mono); 1108 font-size: var(--text-sm); 1109 white-space: pre-wrap; 1110 word-break: break-all; 1111 } 1112 1113 code { 1114 font-family: var(--font-mono); 1115 background: var(--bg-primary); 1116 padding: 2px 6px; 1117 border-radius: var(--radius-sm); 1118 font-size: 0.9em; 1119 } 1120</style>