diff --git a/frontend/src/components/HandleInput.svelte b/frontend/src/components/HandleInput.svelte
index cff1485..518a84c 100644
--- a/frontend/src/components/HandleInput.svelte
+++ b/frontend/src/components/HandleInput.svelte
@@ -1,5 +1,6 @@
{#if show}
-
e.key === 'Escape' && handleClose()} role="presentation">
-
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1">
+
-
-
+
{/if}
diff --git a/frontend/src/components/dashboard/AdminContent.svelte b/frontend/src/components/dashboard/AdminContent.svelte
index d20a592..3d7f1fc 100644
--- a/frontend/src/components/dashboard/AdminContent.svelte
+++ b/frontend/src/components/dashboard/AdminContent.svelte
@@ -12,7 +12,6 @@
setHasLogo as setGlobalHasLogo
} from '../../lib/serverConfig.svelte'
import LoadMoreSentinel from '../LoadMoreSentinel.svelte'
- import { portal } from '../../lib/portal'
interface Props {
session: Session
@@ -48,6 +47,7 @@
let selectedUser = $state(null)
let userActionLoading = $state(false)
let userDetailLoading = $state(false)
+ let userDialog = $state()
let serverName = $state('')
let serverNameInput = $state('')
@@ -309,6 +309,17 @@
selectedUser = null
}
+ $effect(() => {
+ const currentDialog = userDialog
+ if (!currentDialog) return
+ if (selectedUser && !currentDialog.open) {
+ currentDialog.showModal()
+ requestAnimationFrame(() => currentDialog.querySelector('.close-btn')?.focus())
+ } else if (!selectedUser && currentDialog.open) {
+ currentDialog.close()
+ }
+ })
+
async function deleteUserAccount() {
if (!selectedUser) return
if (!confirm($_('admin.deleteConfirm', { values: { handle: selectedUser.handle } }))) return
@@ -502,11 +513,18 @@
{#if selectedUser}
- e.key === 'Escape' && closeUserDetail()} role="presentation">
-
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="dialog" aria-modal="true" tabindex="-1">
+
-
-
+
{/if}
diff --git a/frontend/src/components/dashboard/CommsContent.svelte b/frontend/src/components/dashboard/CommsContent.svelte
index f287364..2f0a69d 100644
--- a/frontend/src/components/dashboard/CommsContent.svelte
+++ b/frontend/src/components/dashboard/CommsContent.svelte
@@ -307,7 +307,14 @@
{#if signalUsername && signalUsername === savedSignalUsername && !signalVerified}
-
+
{/if}
diff --git a/frontend/src/components/dashboard/SecurityContent.svelte b/frontend/src/components/dashboard/SecurityContent.svelte
index 48d5c94..d0c6caa 100644
--- a/frontend/src/components/dashboard/SecurityContent.svelte
+++ b/frontend/src/components/dashboard/SecurityContent.svelte
@@ -3,6 +3,7 @@
import { api, ApiError } from '../../lib/api'
import { _ } from '../../lib/i18n'
import { formatDate } from '../../lib/date'
+ import { getOAuthClientDisplayName } from '../../lib/oauth'
import type { RememberedAppChoice, Session } from '../../lib/types/api'
import { toast } from '../../lib/toast.svelte'
import ReauthModal from '../ReauthModal.svelte'
@@ -175,14 +176,6 @@
}
}
- function appName(clientId: string): string {
- try {
- return new URL(clientId).hostname
- } catch {
- return clientId
- }
- }
-
function startEditDevice(device: TrustedDevice) {
editingDeviceId = device.id
editDeviceName = device.friendlyName || ''
@@ -409,8 +402,10 @@
onclick={handleToggleLegacyLogin}
disabled={legacyLoginUpdating}
aria-label={allowLegacyLogin ? $_('security.disableLegacyLogin') : $_('security.enableLegacyLogin')}
+ role="switch"
+ aria-checked={allowLegacyLogin}
>
-
+
@@ -501,21 +496,36 @@
{:else if rememberedAppChoices.length === 0}
{$_('rememberedApps.none')}
{:else}
-
diff --git a/frontend/src/components/migration/InboundWizard.svelte b/frontend/src/components/migration/InboundWizard.svelte
index e63ee15..0371d8d 100644
--- a/frontend/src/components/migration/InboundWizard.svelte
+++ b/frontend/src/components/migration/InboundWizard.svelte
@@ -275,11 +275,19 @@
flow.setStep('review')
}
- const steps = $derived(isDidWeb
- ? ['Authenticate', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Update DID', 'Complete']
- : flow.state.authMethod === 'passkey'
- ? ['Authenticate', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Passkey', 'App Password', 'Verify PLC', 'Complete']
- : ['Authenticate', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Verify PLC', 'Complete'])
+ const steps = $derived([
+ $_('migration.inbound.sourceAuth.title'),
+ $_('migration.inbound.chooseHandle.title'),
+ $_('migration.inbound.review.title'),
+ $_('migration.inbound.migrating.title'),
+ $_('migration.inbound.emailVerify.title'),
+ ...(flow.state.authMethod === 'passkey'
+ ? [$_('migration.inbound.passkeySetup.title'), $_('migration.inbound.appPassword.title')]
+ : []),
+ isDidWeb ? $_('migration.inbound.didWebUpdate.title') : $_('migration.inbound.plcToken.title'),
+ $_('migration.inbound.finalizing.title'),
+ $_('migration.inbound.success.title'),
+ ])
function getCurrentStepIndex(): number {
const isPasskey = flow.state.authMethod === 'passkey'
@@ -293,9 +301,9 @@
case 'passkey-setup': return isPasskey ? 5 : 4
case 'app-password': return 6
case 'plc-token':
- case 'did-web-update':
- case 'finalizing': return isPasskey ? 7 : 5
- case 'success': return isPasskey ? 8 : 6
+ case 'did-web-update': return isPasskey ? 7 : 5
+ case 'finalizing': return isPasskey ? 8 : 6
+ case 'success': return isPasskey ? 9 : 7
default: return 0
}
}
diff --git a/frontend/src/components/migration/StepIndicator.svelte b/frontend/src/components/migration/StepIndicator.svelte
index 691393f..da0d277 100644
--- a/frontend/src/components/migration/StepIndicator.svelte
+++ b/frontend/src/components/migration/StepIndicator.svelte
@@ -1,22 +1,40 @@
-
- {#each steps as _, i}
-
-
{i < currentIndex ? '✓' : i + 1}
-
- {#if i < steps.length - 1}
-
- {/if}
+
+ {#each steps as step, i}
+ {@const state = i < safeCurrentIndex ? 'completed' : i === safeCurrentIndex ? 'current' : 'upcoming'}
+ -
+ {state === 'completed' ? '✓' : i + 1}
+ {step}
+ {#if i < steps.length - 1}
+
+ {/if}
+
{/each}
-
+
- {steps[currentIndex]} · Step {currentIndex + 1} of {steps.length}
+ {steps[safeCurrentIndex]} · {safeCurrentIndex + 1}/{steps.length}
diff --git a/frontend/src/lib/oauth.ts b/frontend/src/lib/oauth.ts
index e797a91..3087bf4 100644
--- a/frontend/src/lib/oauth.ts
+++ b/frontend/src/lib/oauth.ts
@@ -18,6 +18,14 @@ const CLIENT_ID =
? `${globalThis.location.origin}/oauth-client-metadata.json`
: `http://localhost/?scope=${encodeURIComponent(SCOPES)}`;
+export function getOAuthClientDisplayName(clientId: string): string {
+ try {
+ return new URL(clientId).hostname || clientId;
+ } catch {
+ return clientId;
+ }
+}
+
const REDIRECT_URI = `${globalThis.location.origin}/app/`;
interface OAuthState {
diff --git a/frontend/src/routes/Dashboard.svelte b/frontend/src/routes/Dashboard.svelte
index 5d93863..383910b 100644
--- a/frontend/src/routes/Dashboard.svelte
+++ b/frontend/src/routes/Dashboard.svelte
@@ -33,6 +33,7 @@
let dropdownOpen = $state(false)
let switching = $state(false)
let inviteCodesEnabled = $state(false)
+ let accountTrigger = $state()
function getSession(): Session | null {
return auth.kind === 'authenticated' ? auth.session : null
@@ -109,6 +110,18 @@
navigate(routes.login)
}
+ function closeDropdown(restoreFocus = false) {
+ dropdownOpen = false
+ if (restoreFocus) accountTrigger?.focus()
+ }
+
+ function handleDropdownKeydown(event: KeyboardEvent) {
+ if (dropdownOpen && event.key === 'Escape') {
+ event.preventDefault()
+ closeDropdown(true)
+ }
+ }
+
async function handleSwitchAccount(did: Did) {
switching = true
dropdownOpen = false
@@ -123,7 +136,7 @@
dropdownOpen = !dropdownOpen
}
- function closeDropdown(e: MouseEvent) {
+ function handleDocumentClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.account-dropdown')) {
dropdownOpen = false
@@ -132,12 +145,10 @@
$effect(() => {
if (dropdownOpen) {
- document.addEventListener('click', closeDropdown)
+ document.addEventListener('click', handleDocumentClick)
}
return () => {
- if (dropdownOpen) {
- document.removeEventListener('click', closeDropdown)
- }
+ document.removeEventListener('click', handleDocumentClick)
}
})
@@ -195,6 +206,8 @@
}
+
+
{#if session}