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>