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} - - + {/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} - - + {/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} -
+
    {#each rememberedAppChoices as choice} -
    -
    - {appName(choice.clientId)} -
    -
    - {choice.scopes.join(' ')} + {@const name = getOAuthClientDisplayName(choice.clientId)} +
  • +
    +
    + {name} + {#if name !== choice.clientId} + {choice.clientId} + {/if} +
    +
    - -
  • + +
    + + {$_('appPasswords.permissions')} + {choice.scopes.length} + +
      + {#each choice.scopes as scope} +
    • {scope}
    • + {/each} +
    +
    + {/each} -
    +
{/if}
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'} +
  1. + + + {#if i < steps.length - 1} + + {/if} +
  2. {/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}
@@ -299,7 +321,7 @@

{currentSection ? getSectionTitle(currentSection) : ''}

diff --git a/frontend/src/routes/Login.svelte b/frontend/src/routes/Login.svelte index 955fecb..04e821a 100644 --- a/frontend/src/routes/Login.svelte +++ b/frontend/src/routes/Login.svelte @@ -168,22 +168,26 @@ {/each} @@ -192,7 +196,7 @@

{$_('login.signInToAnother')}

{/if} - diff --git a/frontend/src/routes/OAuthAccounts.svelte b/frontend/src/routes/OAuthAccounts.svelte index f419790..d6e9233 100644 --- a/frontend/src/routes/OAuthAccounts.svelte +++ b/frontend/src/routes/OAuthAccounts.svelte @@ -143,7 +143,8 @@ type="button" class="account-item" class:disabled={submitting} - onclick={() => !submitting && handleSelectAccount(account.did)} + onclick={() => handleSelectAccount(account.did)} + disabled={submitting} >