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