diff --git a/frontend/src/components/migration/ChooseHandleStep.svelte b/frontend/src/components/migration/ChooseHandleStep.svelte index 71f19f2..7d50dfe 100644 --- a/frontend/src/components/migration/ChooseHandleStep.svelte +++ b/frontend/src/components/migration/ChooseHandleStep.svelte @@ -1,34 +1,44 @@

{$_('migration.inbound.emailVerify.title')}

-

{@html $_('migration.inbound.emailVerify.desc', { values: { email: `${email}` } })}

-
-

- {$_('migration.inbound.emailVerify.hint')} -

-
- - {#if error} -
- {error} + {#if isTelegram && telegramBotUsername && handle} + {@const encodedHandle = handle.replaceAll('.', '_')} +

{$_('migration.inbound.emailVerify.telegramInstructions')}

+
+

+ {$_('migration.inbound.emailVerify.openTelegram')}, + or send /start {handle} to @{telegramBotUsername} +

+

{$_('migration.inbound.emailVerify.waitingForVerification')}

+ {:else if isDiscord && discordAppId && handle} +

{$_('migration.inbound.emailVerify.discordInstructions')}

+
+

+ {$_('migration.inbound.emailVerify.openDiscord')}, + or send /start {handle} to {discordBotUsername ?? 'the bot'} +

+
+

{$_('migration.inbound.emailVerify.waitingForVerification')}

+ {:else} +

{@html $_('migration.inbound.emailVerify.desc', { values: { email: `${identifier}`, channel: channelLabel(channel) } })}

+ +
+

+ {$_('migration.inbound.emailVerify.hint')} +

+
+ + {#if error} +
+ {error} +
+ {/if} + +
+
+ + onTokenChange((e.target as HTMLInputElement).value)} + disabled={loading} + required + /> +
+ +
+ + +
+
{/if} - -
-
- - onTokenChange((e.target as HTMLInputElement).value)} - disabled={loading} - required - /> -
- -
- - -
-
diff --git a/frontend/src/components/migration/InboundWizard.svelte b/frontend/src/components/migration/InboundWizard.svelte index 3e363f9..73a1244 100644 --- a/frontend/src/components/migration/InboundWizard.svelte +++ b/frontend/src/components/migration/InboundWizard.svelte @@ -1,8 +1,9 @@
-
- {#each steps as _, i} -
-
{i < getCurrentStepIndex() ? '✓' : i + 1}
-
- {#if i < steps.length - 1} -
- {/if} - {/each} -
-
- {steps[getCurrentStepIndex()]} · Step {getCurrentStepIndex() + 1} of {steps.length} -
+ {#if flow.state.error}
{flow.state.error}
@@ -443,29 +398,37 @@ flow.checkHandleAvailability(h)} onHandleChange={(h) => handleInput = h} onDomainChange={(d) => selectedDomain = d} - onCheckHandle={checkHandle} onEmailChange={(e) => flow.updateField('targetEmail', e)} onPasswordChange={(p) => flow.updateField('targetPassword', p)} onAuthMethodChange={(m) => selectedAuthMethod = m} onInviteCodeChange={(c) => flow.updateField('inviteCode', c)} + onVerificationChannelChange={(ch) => flow.updateField('verificationChannel', ch)} + onDiscordChange={(v) => flow.updateField('discordUsername', v)} + onTelegramChange={(v) => flow.updateField('telegramUsername', v)} + onSignalChange={(v) => flow.updateField('signalUsername', v)} onHandlePreservationChange={handlePreservationChange} onVerifyExistingHandle={verifyExistingHandle} onBack={() => flow.setStep('source-handle')} @@ -473,88 +436,39 @@ /> {:else if flow.state.step === 'review'} -
-

{$_('migration.inbound.review.title')}

-

{$_('migration.inbound.review.desc')}

- -
-
- {$_('migration.inbound.review.currentHandle')}: - {flow.state.sourceHandle} -
-
- {$_('migration.inbound.review.newHandle')}: - {flow.state.targetHandle} -
-
- {$_('migration.inbound.review.did')}: - {flow.state.sourceDid} -
-
- {$_('migration.inbound.review.sourcePds')}: - {flow.state.sourcePdsUrl} -
-
- {$_('migration.inbound.review.targetPds')}: - {window.location.origin} -
-
- {$_('migration.inbound.review.email')}: - {flow.state.targetEmail} -
-
- {$_('migration.inbound.review.authentication')}: - {flow.state.authMethod === 'passkey' ? $_('migration.inbound.review.authPasskey') : $_('migration.inbound.review.authPassword')} -
-
- -
+ flow.setStep('choose-handle')} + onContinue={startMigration} + > + {#snippet warning()} {$_('migration.inbound.review.warning')} -
- -
- - -
-
+ {/snippet} + {:else if flow.state.step === 'migrating'} -
-

{$_('migration.inbound.migrating.title')}

-

{$_('migration.inbound.migrating.desc')}

- -
-
- {flow.state.progress.repoExported ? '✓' : '○'} - {$_('migration.inbound.migrating.exportRepo')} -
-
- {flow.state.progress.repoImported ? '✓' : '○'} - {$_('migration.inbound.migrating.importRepo')} -
-
- {flow.state.progress.blobsMigrated === flow.state.progress.blobsTotal && flow.state.progress.blobsTotal > 0 ? '✓' : '○'} - {$_('migration.inbound.migrating.migrateBlobs')} ({flow.state.progress.blobsMigrated}/{flow.state.progress.blobsTotal}) -
-
- {flow.state.progress.prefsMigrated ? '✓' : '○'} - {$_('migration.inbound.migrating.migratePrefs')} -
-
- - {#if flow.state.progress.blobsTotal > 0} -
-
-
- {/if} - -

{flow.state.progress.currentOperation}

-
+ 0, active: flow.state.progress.repoImported && !flow.state.progress.prefsMigrated }, + { label: $_('migration.inbound.migrating.migratePrefs'), completed: flow.state.progress.prefsMigrated }, + ]} + statusText={flow.state.progress.currentOperation} + progressBar={flow.state.progress.blobsTotal > 0 ? { current: flow.state.progress.blobsMigrated, total: flow.state.progress.blobsTotal } : undefined} + /> {:else if flow.state.step === 'passkey-setup'} {:else if flow.state.step === 'finalizing'} -
-

{$_('migration.inbound.finalizing.title')}

-

{$_('migration.inbound.finalizing.desc')}

- -
-
- {flow.state.progress.plcSigned ? '✓' : '○'} - {$_('migration.inbound.finalizing.signingPlc')} -
-
- {flow.state.progress.activated ? '✓' : '○'} - {$_('migration.inbound.finalizing.activating')} -
-
- {flow.state.progress.deactivated ? '✓' : '○'} - {$_('migration.inbound.finalizing.deactivating')} -
-
- -

{flow.state.progress.currentOperation}

-
+ {:else if flow.state.step === 'success'} diff --git a/frontend/src/components/migration/OfflineInboundWizard.svelte b/frontend/src/components/migration/OfflineInboundWizard.svelte index 6df2922..177c532 100644 --- a/frontend/src/components/migration/OfflineInboundWizard.svelte +++ b/frontend/src/components/migration/OfflineInboundWizard.svelte @@ -1,8 +1,9 @@
-
- {#each steps as _, i} -
-
{i < getCurrentStepIndex() ? '✓' : i + 1}
-
- {#if i < steps.length - 1} -
- {/if} - {/each} -
-
- {steps[getCurrentStepIndex()]} · Step {getCurrentStepIndex() + 1} of {steps.length} -
+ {#if flow.state.error}
{flow.state.error}
@@ -401,13 +377,16 @@ flow.checkHandleAvailability(h)} onHandleChange={(h) => handleInput = h} onDomainChange={(d) => selectedDomain = d} - onCheckHandle={checkHandle} onEmailChange={(e) => flow.setTargetEmail(e)} onPasswordChange={(p) => flow.setTargetPassword(p)} onAuthMethodChange={(m) => selectedAuthMethod = m} onInviteCodeChange={(c) => flow.setInviteCode(c)} + onVerificationChannelChange={(ch) => flow.updateField('verificationChannel', ch)} + onDiscordChange={(v) => flow.updateField('discordUsername', v)} + onTelegramChange={(v) => flow.updateField('telegramUsername', v)} + onSignalChange={(v) => flow.updateField('signalUsername', v)} onBack={() => flow.setStep('provide-rotation-key')} onContinue={proceedToReview} /> {:else if flow.state.step === 'review'} -
-

{$_('migration.inbound.review.title')}

-

{$_('migration.offline.review.desc')}

- -
-
- {$_('migration.inbound.review.did')}: - {flow.state.userDid} -
-
- {$_('migration.inbound.review.newHandle')}: - {flow.state.targetHandle} -
-
- {$_('migration.offline.review.carFile')}: - {flow.state.carFileName} ({(flow.state.carSizeBytes / 1024 / 1024).toFixed(2)} MB) -
-
- {$_('migration.offline.review.rotationKey')}: - {flow.state.rotationKeyDidKey} -
-
- {$_('migration.inbound.review.targetPds')}: - {window.location.origin} -
-
- {$_('migration.inbound.review.email')}: - {flow.state.targetEmail} -
-
- {$_('migration.inbound.review.authentication')}: - {flow.state.authMethod === 'passkey' ? $_('migration.inbound.review.authPasskey') : $_('migration.inbound.review.authPassword')} -
-
- -
+ flow.setStep('choose-handle')} + onContinue={startMigration} + > + {#snippet warning()} {$_('migration.offline.review.plcWarningTitle')}

{$_('migration.offline.review.plcWarning')}

-
- -
- - -
-
+ {/snippet} + {:else if flow.state.step === 'creating' || flow.state.step === 'importing'} -
-

{$_('migration.offline.migrating.title')}

-

{$_('migration.offline.migrating.desc')}

- -
-
- {flow.state.step !== 'creating' ? '✓' : '○'} - {$_('migration.offline.migrating.creating')} -
-
- - {$_('migration.offline.migrating.importing')} -
-
- -

{flow.state.progress.currentOperation}

-
+ {:else if flow.state.step === 'migrating-blobs'} -
-

{$_('migration.offline.blobs.title')}

-

{$_('migration.offline.blobs.desc')}

- -
-
- - {$_('migration.offline.migrating.importing')} -
-
- - {$_('migration.offline.blobs.migrating')} -
-
- - {#if flow.state.progress.blobsTotal > 0} -
-
-
-
-

- {flow.state.progress.blobsMigrated} / {flow.state.progress.blobsTotal} blobs -

-
- {/if} - -

{flow.state.progress.currentOperation}

- + 0 ? { current: flow.state.progress.blobsMigrated, total: flow.state.progress.blobsTotal } : undefined} + > {#if flow.state.progress.blobsFailed.length > 0}
{$_('migration.offline.blobs.failedTitle')}

{$_('migration.offline.blobs.failedDesc', { values: { count: flow.state.progress.blobsFailed.length } })}

{/if} -
+
{:else if flow.state.step === 'email-verify'} {:else if flow.state.step === 'plc-signing' || flow.state.step === 'finalizing'} -
-

{$_('migration.inbound.finalizing.title')}

-

{$_('migration.inbound.finalizing.desc')}

- -
-
- {flow.state.progress.plcSigned ? '✓' : '○'} - {$_('migration.inbound.finalizing.signingPlc')} -
-
- {flow.state.progress.activated ? '✓' : '○'} - {$_('migration.inbound.finalizing.activating')} -
-
- -

{flow.state.progress.currentOperation}

-
+ {:else if flow.state.step === 'success'} + import type { Snippet } from 'svelte' + + interface ProgressItem { + label: string + completed: boolean + active?: boolean + } + + interface Props { + title: string + description: string + items: ProgressItem[] + statusText: string + progressBar?: { current: number; total: number } + children?: Snippet + } + + let { title, description, items, statusText, progressBar, children }: Props = $props() + + +
+

{title}

+

{description}

+ +
+ {#each items as item} +
+ {item.completed ? '✓' : '○'} + {item.label} +
+ {/each} +
+ + {#if progressBar && progressBar.total > 0} +
+
+
+ {/if} + +

{statusText}

+ + {#if children} + {@render children()} + {/if} +
diff --git a/frontend/src/components/migration/ReviewStep.svelte b/frontend/src/components/migration/ReviewStep.svelte new file mode 100644 index 0000000..0668853 --- /dev/null +++ b/frontend/src/components/migration/ReviewStep.svelte @@ -0,0 +1,48 @@ + + +
+

{$_('migration.inbound.review.title')}

+

{description}

+ +
+ {#each rows as row} +
+ {row.label}: + {row.value} +
+ {/each} +
+ + {#if warning} +
+ {@render warning()} +
+ {/if} + +
+ + +
+
diff --git a/frontend/src/components/migration/StepIndicator.svelte b/frontend/src/components/migration/StepIndicator.svelte new file mode 100644 index 0000000..691393f --- /dev/null +++ b/frontend/src/components/migration/StepIndicator.svelte @@ -0,0 +1,22 @@ + + +
+ {#each steps as _, i} +
+
{i < currentIndex ? '✓' : i + 1}
+
+ {#if i < steps.length - 1} +
+ {/if} + {/each} +
+
+ {steps[currentIndex]} · Step {currentIndex + 1} of {steps.length} +
diff --git a/frontend/src/styles/migration.css b/frontend/src/styles/migration.css index 12a0b92..7bf6656 100644 --- a/frontend/src/styles/migration.css +++ b/frontend/src/styles/migration.css @@ -92,12 +92,6 @@ margin: 0 0 var(--space-5) 0; } -.info-box { - background: var(--accent-muted); - padding: var(--space-5); - margin-bottom: var(--space-5); -} - .info-box h3 { margin: 0 0 var(--space-3) 0; font-size: var(--text-base); @@ -119,13 +113,6 @@ color: var(--text-secondary); } -.warning-box { - background: var(--warning-bg); - padding: var(--space-5); - margin-bottom: var(--space-5); - font-size: var(--text-sm); -} - .warning-box strong { color: var(--warning-text); } @@ -158,6 +145,7 @@ .button-row { display: flex; + flex-direction: row; gap: var(--space-3); justify-content: flex-end; margin-top: var(--space-5); @@ -260,28 +248,6 @@ font-size: var(--text-sm); } -.blob-progress { - margin: var(--space-4) 0; -} - -.blob-progress-bar { - height: 8px; - background: var(--bg-primary); - overflow: hidden; - margin-bottom: var(--space-2); -} - -.blob-progress-fill { - height: 100%; - background: var(--accent); -} - -.blob-progress-text { - text-align: center; - color: var(--text-secondary); - font-size: var(--text-sm); - margin: 0; -} .success-content { text-align: center; @@ -411,43 +377,6 @@ label.auth-option { color: var(--text-secondary); } -.app-password-display { - background: var(--bg-primary); - padding: var(--space-5); - margin-bottom: var(--space-5); - text-align: center; -} - -.app-password-label { - font-size: var(--text-sm); - color: var(--text-secondary); - margin-bottom: var(--space-3); -} - -.app-password-code { - display: block; - font-family: var(--font-mono); - font-size: var(--text-lg); - letter-spacing: 0.1em; - padding: var(--space-4); - background: var(--bg-tertiary); - margin-bottom: var(--space-4); - user-select: all; -} - -.copy-btn { - font-size: var(--text-sm); -} - -.current-account { - background: var(--bg-primary); - padding: var(--space-4); - margin-bottom: var(--space-5); - display: flex; - justify-content: space-between; - align-items: center; -} - .current-account .label { color: var(--text-secondary); } @@ -457,12 +386,6 @@ label.auth-option { font-size: var(--text-lg); } -.server-info { - background: var(--bg-primary); - padding: var(--space-4); - margin-top: var(--space-5); -} - .server-info h3 { margin: 0 0 var(--space-3) 0; font-size: var(--text-base); @@ -488,11 +411,6 @@ label.auth-option { font-size: var(--text-sm); } -.final-warning { - background: var(--error-bg); - border-color: var(--error-border); -} - .final-warning strong { color: var(--error-text); } @@ -596,16 +514,6 @@ label.auth-option { margin-bottom: var(--space-4); } -.message.success { - background: var(--success-bg); - color: var(--success-text); -} - -.message.error { - background: var(--error-bg); - color: var(--error-text); -} - .handle-choice-options { display: flex; flex-direction: column;