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