refactor(frontend): delete RegisterPassword and UiTest routes

This commit is contained in:
Lewis
2026-03-19 14:26:26 +00:00
committed by Tangled
parent b990c239e6
commit 6225d2bc1d
2 changed files with 0 additions and 1084 deletions
-550
View File
@@ -1,550 +0,0 @@
<script lang="ts">
import { navigate, routes, getFullUrl } from '../lib/router.svelte'
import { api, ApiError } from '../lib/api'
import { _ } from '../lib/i18n'
import {
createRegistrationFlow,
restoreRegistrationFlow,
VerificationStep,
KeyChoiceStep,
DidDocStep,
} from '../lib/registration'
import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte'
import HandleInput from '../components/HandleInput.svelte'
import { ensureRequestUri, getRequestUriFromUrl } from '../lib/oauth'
let serverInfo = $state<{
availableUserDomains: string[]
inviteCodeRequired: boolean
availableCommsChannels?: string[]
selfHostedDidWebEnabled?: boolean
} | null>(null)
let loadingServerInfo = $state(true)
let serverInfoLoaded = false
let ssoAvailable = $state(false)
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
let confirmPassword = $state('')
let clientName = $state<string | null>(null)
let selectedDomain = $state('')
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
$effect(() => {
if (!flow) return
const handle = flow.info.handle
if (checkHandleTimeout) {
clearTimeout(checkHandleTimeout)
}
if (handle.length >= 3 && !handle.includes('.')) {
checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400)
}
})
$effect(() => {
if (!serverInfoLoaded) {
serverInfoLoaded = true
ensureRequestUri().then((requestUri) => {
if (!requestUri) return
loadServerInfo()
checkSsoAvailable()
fetchClientName()
}).catch((err) => {
console.error('Failed to ensure OAuth request URI:', err)
})
}
})
async function fetchClientName() {
const requestUri = getRequestUriFromUrl()
if (!requestUri) return
try {
const response = await fetch(`/oauth/authorize?request_uri=${encodeURIComponent(requestUri)}`, {
headers: { 'Accept': 'application/json' }
})
if (response.ok) {
const data = await response.json()
clientName = data.client_name || null
}
} catch {
clientName = null
}
}
async function checkSsoAvailable() {
try {
const response = await fetch('/oauth/sso/providers')
if (response.ok) {
const data = await response.json()
ssoAvailable = (data.providers?.length ?? 0) > 0
}
} catch {
ssoAvailable = false
}
}
$effect(() => {
if (flow?.state.step === 'redirect-to-dashboard') {
completeOAuthRegistration()
}
})
let creatingStarted = false
$effect(() => {
if (flow?.state.step === 'creating' && !creatingStarted) {
creatingStarted = true
flow.createPasswordAccount()
}
})
async function loadServerInfo() {
try {
const restored = restoreRegistrationFlow()
if (restored && restored.state.mode === 'password') {
flow = restored
serverInfo = await api.describeServer()
} else {
serverInfo = await api.describeServer()
const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname
flow = createRegistrationFlow('password', hostname)
}
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
if (flow) flow.setSelectedDomain(selectedDomain)
} catch (e) {
console.error('Failed to load server info:', e)
} finally {
loadingServerInfo = false
}
}
function validateInfoStep(): string | null {
if (!flow) return 'Flow not initialized'
const info = flow.info
if (!info.handle.trim()) return $_('register.validation.handleRequired')
if (info.handle.includes('.')) return $_('register.validation.handleNoDots')
if (!info.password) return $_('register.validation.passwordRequired')
if (info.password.length < 8) return $_('register.validation.passwordLength')
if (info.password !== confirmPassword) return $_('register.validation.passwordsMismatch')
if (serverInfo?.inviteCodeRequired && !info.inviteCode?.trim()) {
return $_('register.validation.inviteCodeRequired')
}
if (info.didType === 'web-external') {
if (!info.externalDid?.trim()) return $_('register.validation.externalDidRequired')
if (!info.externalDid.trim().startsWith('did:web:')) return $_('register.validation.externalDidFormat')
}
switch (info.verificationChannel) {
case 'email':
if (!info.email.trim()) return $_('register.validation.emailRequired')
break
case 'discord':
if (!info.discordUsername?.trim()) return $_('register.validation.discordUsernameRequired')
break
case 'telegram':
if (!info.telegramUsername?.trim()) return $_('register.validation.telegramRequired')
break
case 'signal':
if (!info.signalUsername?.trim()) return $_('register.validation.signalRequired')
break
}
return null
}
async function handleInfoSubmit(e: Event) {
e.preventDefault()
if (!flow) return
const validationError = validateInfoStep()
if (validationError) {
flow.setError(validationError)
return
}
flow.clearError()
flow.proceedFromInfo()
}
async function handleCreateAccount() {
if (!flow) return
await flow.createPasswordAccount()
}
async function handleComplete() {
if (flow) {
await flow.finalizeSession()
}
navigate(routes.dashboard)
}
async function completeOAuthRegistration() {
const requestUri = getRequestUriFromUrl()
if (!requestUri || !flow?.account) {
navigate(routes.dashboard)
return
}
try {
const response = await fetch('/oauth/register/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
request_uri: requestUri,
did: flow.account.did,
app_password: flow.account.appPassword || flow.info.password,
}),
})
const data = await response.json()
if (!response.ok) {
flow.setError(data.error_description || data.error || $_('common.error'))
return
}
if (data.redirect_uri) {
window.location.href = data.redirect_uri
return
}
navigate(routes.dashboard)
} catch (err) {
console.error('OAuth registration completion failed:', err)
flow.setError(err instanceof Error ? err.message : $_('common.error'))
}
}
function isChannelAvailable(ch: string): boolean {
const available = serverInfo?.availableCommsChannels ?? ['email']
return available.includes(ch)
}
function channelLabel(ch: string): string {
switch (ch) {
case 'email': return $_('register.email')
case 'discord': return $_('register.discord')
case 'telegram': return $_('register.telegram')
case 'signal': return $_('register.signal')
default: return ch
}
}
let fullHandle = $derived(() => {
if (!flow?.info.handle.trim()) return ''
if (flow.info.handle.includes('.')) return flow.info.handle.trim()
return selectedDomain ? `${flow.info.handle.trim()}.${selectedDomain}` : flow.info.handle.trim()
})
function extractDomain(did: string): string {
return did.replace('did:web:', '').replace(/%3A/g, ':')
}
async function handleCancel() {
const requestUri = getRequestUriFromUrl()
if (!requestUri) {
window.history.back()
return
}
try {
const response = await fetch('/oauth/authorize/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ request_uri: requestUri })
})
if (!response.ok) {
window.history.back()
return
}
const data = await response.json()
if (data.redirect_uri) {
window.location.href = data.redirect_uri
} else {
window.history.back()
}
} catch {
window.history.back()
}
}
</script>
<div class="page">
<header class="page-header">
<h1>{$_('register.title')}</h1>
{#if clientName}
<p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p>
{/if}
</header>
{#if flow?.state.error}
<div class="message error">{flow.state.error}</div>
{/if}
{#if loadingServerInfo || !flow}
<div class="loading"></div>
{:else if flow.state.step === 'info'}
<div class="migrate-callout">
<div class="migrate-icon"></div>
<div class="migrate-content">
<strong>{$_('register.migrateTitle')}</strong>
<p>{$_('register.migrateDescription')}</p>
<a href={getFullUrl(routes.migrate)} class="migrate-link">
{$_('register.migrateLink')}
</a>
</div>
</div>
<AccountTypeSwitcher active="password" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} />
<form class="register-form" onsubmit={handleInfoSubmit}>
<div>
<label for="handle">{$_('register.handle')}</label>
<HandleInput
value={flow.info.handle}
domains={serverInfo?.availableUserDomains ?? []}
{selectedDomain}
placeholder={$_('register.handlePlaceholder')}
disabled={flow.state.submitting}
onInput={(v) => { flow!.info.handle = v }}
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
/>
{#if flow.info.handle.includes('.')}
<p class="hint warning">{$_('register.handleDotWarning')}</p>
{:else if flow.state.checkingHandle}
<p class="hint">{$_('common.checking')}</p>
{:else if flow.state.handleAvailable === false}
<p class="hint warning">{$_('register.handleTaken')}</p>
{:else if flow.state.handleAvailable === true && fullHandle()}
<p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{:else if fullHandle()}
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
{/if}
</div>
<div>
<label for="password">{$_('register.password')}</label>
<input
id="password"
type="password"
bind:value={flow.info.password}
placeholder={$_('register.passwordPlaceholder')}
disabled={flow.state.submitting}
required
minlength="8"
/>
</div>
<div>
<label for="confirm-password">{$_('register.confirmPassword')}</label>
<input
id="confirm-password"
type="password"
bind:value={confirmPassword}
placeholder={$_('register.confirmPasswordPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
<div>
<label for="verification-channel">{$_('register.verificationMethod')}</label>
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
<option value="email">{$_('register.email')}</option>
{#if isChannelAvailable('discord')}
<option value="discord">{$_('register.discord')}</option>
{/if}
{#if isChannelAvailable('telegram')}
<option value="telegram">{$_('register.telegram')}</option>
{/if}
{#if isChannelAvailable('signal')}
<option value="signal">{$_('register.signal')}</option>
{/if}
</select>
</div>
{#if flow.info.verificationChannel === 'email'}
<div>
<label for="email">{$_('register.emailAddress')}</label>
<input
id="email"
type="email"
bind:value={flow.info.email}
placeholder={$_('register.emailPlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{:else if flow.info.verificationChannel === 'discord'}
<div>
<label for="discord-username">{$_('register.discordUsername')}</label>
<input
id="discord-username"
type="text"
bind:value={flow.info.discordUsername}
onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordUsername ?? '')}
placeholder={$_('register.discordUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.state.discordInUse}
<p class="hint warning">{$_('register.discordInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'telegram'}
<div>
<label for="telegram-username">{$_('register.telegramUsername')}</label>
<input
id="telegram-username"
type="text"
bind:value={flow.info.telegramUsername}
onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')}
placeholder={$_('register.telegramUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
{#if flow.state.telegramInUse}
<p class="hint warning">{$_('register.telegramInUseWarning')}</p>
{/if}
</div>
{:else if flow.info.verificationChannel === 'signal'}
<div>
<label for="signal-number">{$_('register.signalUsername')}</label>
<input
id="signal-number"
type="tel"
bind:value={flow.info.signalUsername}
onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalUsername ?? '')}
placeholder={$_('register.signalUsernamePlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.signalUsernameHint')}</p>
{#if flow.state.signalInUse}
<p class="hint warning">{$_('register.signalInUseWarning')}</p>
{/if}
</div>
{/if}
<fieldset class="identity-section">
<legend>{$_('register.identityType')}</legend>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didPlc')}</strong>
<span class="radio-hint">{$_('register.didPlcHint')}</span>
</span>
</label>
<label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}>
<input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} />
<span class="radio-content">
<strong>{$_('register.didWeb')}</strong>
{#if serverInfo?.selfHostedDidWebEnabled === false}
<span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span>
{:else}
<span class="radio-hint">{$_('register.didWebHint')}</span>
{/if}
</span>
</label>
<label class="radio-label">
<input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} />
<span class="radio-content">
<strong>{$_('register.didWebBYOD')}</strong>
<span class="radio-hint">{$_('register.didWebBYODHint')}</span>
</span>
</label>
</div>
</fieldset>
{#if flow.info.didType === 'web'}
<div class="warning-box">
<strong>{$_('register.didWebWarningTitle')}</strong>
<ul>
<li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li>
<li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li>
{#if $_('register.didWebWarning3')}
<li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li>
{/if}
</ul>
</div>
{/if}
{#if flow.info.didType === 'web-external'}
<div>
<label for="external-did">{$_('register.externalDid')}</label>
<input
id="external-did"
type="text"
bind:value={flow.info.externalDid}
placeholder={$_('register.externalDidPlaceholder')}
disabled={flow.state.submitting}
required
/>
<p class="hint">{$_('register.externalDidHint')}</p>
</div>
{/if}
{#if serverInfo?.inviteCodeRequired}
<div>
<label for="invite-code">{$_('register.inviteCode')}</label>
<input
id="invite-code"
type="text"
bind:value={flow.info.inviteCode}
placeholder={$_('register.inviteCodePlaceholder')}
disabled={flow.state.submitting}
required
/>
</div>
{/if}
<div class="form-actions">
<button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}>
{$_('common.cancel')}
</button>
<button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}>
{flow.state.submitting ? $_('common.loading') : $_('common.continue')}
</button>
</div>
</form>
{:else if flow.state.step === 'key-choice'}
<KeyChoiceStep {flow} />
{:else if flow.state.step === 'initial-did-doc'}
<DidDocStep
{flow}
type="initial"
onConfirm={handleCreateAccount}
onBack={() => flow?.goBack()}
/>
{:else if flow.state.step === 'creating'}
<div class="loading">
<p>{$_('common.creating')}</p>
</div>
{:else if flow.state.step === 'verify'}
<VerificationStep {flow} />
{:else if flow.state.step === 'updated-did-doc'}
<DidDocStep
{flow}
type="updated"
onConfirm={() => flow?.activateAccount()}
/>
{:else if flow.state.step === 'redirect-to-dashboard'}
<div class="loading">
<p>{$_('register.redirecting')}</p>
</div>
{/if}
</div>
-534
View File
@@ -1,534 +0,0 @@
<script lang="ts">
import { Button, Card, Input, Message, Page, Section } from '../components/ui'
import Skeleton from '../components/Skeleton.svelte'
import { toast } from '../lib/toast.svelte'
import { getServerConfigState } from '../lib/serverConfig.svelte'
import { _, locale, getSupportedLocales, localeNames, type SupportedLocale } from '../lib/i18n'
let inputValue = $state('')
let inputError = $state('')
let inputDisabled = $state('')
const serverConfig = getServerConfigState()
const LIGHT_ACCENT_DEFAULT = '#1a1d1d'
const DARK_ACCENT_DEFAULT = '#e6e8e8'
const LIGHT_SECONDARY_DEFAULT = '#1a1d1d'
const DARK_SECONDARY_DEFAULT = '#e6e8e8'
let accentLight = $state(LIGHT_ACCENT_DEFAULT)
let accentDark = $state(DARK_ACCENT_DEFAULT)
let secondaryLight = $state(LIGHT_SECONDARY_DEFAULT)
let secondaryDark = $state(DARK_SECONDARY_DEFAULT)
$effect(() => {
accentLight = serverConfig.primaryColor || LIGHT_ACCENT_DEFAULT
accentDark = serverConfig.primaryColorDark || DARK_ACCENT_DEFAULT
secondaryLight = serverConfig.secondaryColor || LIGHT_SECONDARY_DEFAULT
secondaryDark = serverConfig.secondaryColorDark || DARK_SECONDARY_DEFAULT
})
const isDark = $derived(
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
)
function applyColor(prop: string, value: string): void {
document.documentElement.style.setProperty(prop, value)
}
$effect(() => {
applyColor('--accent', isDark ? accentDark : accentLight)
})
$effect(() => {
applyColor('--secondary', isDark ? secondaryDark : secondaryLight)
})
</script>
<Page title="UI Test" size="lg">
<Section title="Theme">
<div class="form-row">
<div class="field">
<label for="accent-light">Accent (light)</label>
<div class="color-pair">
<input type="color" bind:value={accentLight} />
<input id="accent-light" type="text" class="mono" bind:value={accentLight} />
</div>
</div>
<div class="field">
<label for="accent-dark">Accent (dark)</label>
<div class="color-pair">
<input type="color" bind:value={accentDark} />
<input id="accent-dark" type="text" class="mono" bind:value={accentDark} />
</div>
</div>
<div class="field">
<label for="secondary-light">Secondary (light)</label>
<div class="color-pair">
<input type="color" bind:value={secondaryLight} />
<input id="secondary-light" type="text" class="mono" bind:value={secondaryLight} />
</div>
</div>
<div class="field">
<label for="secondary-dark">Secondary (dark)</label>
<div class="color-pair">
<input type="color" bind:value={secondaryDark} />
<input id="secondary-dark" type="text" class="mono" bind:value={secondaryDark} />
</div>
</div>
<div class="field">
<label for="locale-picker">Locale</label>
<select id="locale-picker" value={$locale} onchange={(e) => locale.set(e.currentTarget.value)}>
{#each getSupportedLocales() as loc}
<option value={loc}>{localeNames[loc]} ({loc})</option>
{/each}
</select>
</div>
</div>
</Section>
<Section title="Typography">
<p style="font-size: var(--text-4xl)">4xl (2.5rem)</p>
<p style="font-size: var(--text-3xl)">3xl (2rem)</p>
<p style="font-size: var(--text-2xl)">2xl (1.5rem)</p>
<p style="font-size: var(--text-xl)">xl (1.25rem)</p>
<p style="font-size: var(--text-lg)">lg (1.125rem)</p>
<p style="font-size: var(--text-base)">base (1rem)</p>
<p style="font-size: var(--text-sm)">sm (0.875rem)</p>
<p style="font-size: var(--text-xs)">xs (0.75rem)</p>
<hr />
<p style="font-weight: var(--font-normal)">Normal (400)</p>
<p style="font-weight: var(--font-medium)">Medium (500)</p>
<p style="font-weight: var(--font-semibold)">Semibold (600)</p>
<p style="font-weight: var(--font-bold)">Bold (700)</p>
<hr />
<code>Monospace text</code>
<pre>Pre block
indented</pre>
</Section>
<Section title="Colors">
<div class="form-row">
<div>
<h4>Backgrounds</h4>
<div class="swatch" style="background: var(--bg-primary)">bg-primary</div>
<div class="swatch" style="background: var(--bg-secondary)">bg-secondary</div>
<div class="swatch" style="background: var(--bg-tertiary)">bg-tertiary</div>
<div class="swatch" style="background: var(--bg-card)">bg-card</div>
<div class="swatch" style="background: var(--bg-input)">bg-input</div>
</div>
<div>
<h4>Text</h4>
<p style="color: var(--text-primary)">text-primary</p>
<p style="color: var(--text-secondary)">text-secondary</p>
<p style="color: var(--text-muted)">text-muted</p>
<div class="swatch" style="background: var(--accent); color: var(--text-inverse)">text-inverse</div>
</div>
<div>
<h4>Accent</h4>
<div class="swatch" style="background: var(--accent); color: var(--text-inverse)">accent</div>
<div class="swatch" style="background: var(--accent-hover); color: var(--text-inverse)">accent-hover</div>
<div class="swatch" style="background: var(--accent-muted)">accent-muted</div>
</div>
<div>
<h4>Status</h4>
<div class="swatch" style="background: var(--success-bg); color: var(--success-text)">success</div>
<div class="swatch" style="background: var(--error-bg); color: var(--error-text)">error</div>
<div class="swatch" style="background: var(--warning-bg); color: var(--warning-text)">warning</div>
</div>
</div>
</Section>
<Section title="Spacing">
<div class="spacing-row">
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as i}
<div class="spacing-item">
<div class="spacing-box" style="width: var(--space-{i}); height: var(--space-{i})"></div>
<span class="text-xs text-muted">--space-{i}</span>
</div>
{/each}
</div>
</Section>
<Section title="Buttons">
<p>
<Button variant="primary">{$_('common.save')}</Button>
<Button variant="secondary">{$_('common.cancel')}</Button>
<Button variant="tertiary">{$_('common.back')}</Button>
<Button variant="danger">{$_('common.delete')}</Button>
<Button variant="ghost">{$_('common.refresh')}</Button>
</p>
<p class="mt-5">
<Button size="sm">{$_('common.verify')}</Button>
<Button size="md">{$_('common.continue')}</Button>
<Button size="lg">{$_('common.signIn')}</Button>
</p>
<p class="mt-5">
<Button disabled>{$_('common.save')}</Button>
<Button loading>{$_('common.saving')}</Button>
<Button variant="secondary" disabled>{$_('common.cancel')}</Button>
<Button variant="danger" loading>{$_('common.delete')}</Button>
</p>
<p class="mt-5">
<button class="danger-outline">{$_('common.revoke')}</button>
<button class="link">{$_('login.forgotPassword')}</button>
<button class="sm">{$_('common.done')}</button>
</p>
<div class="mt-5">
<Button fullWidth>{$_('common.signIn')}</Button>
</div>
</Section>
<Section title="Inputs">
<div class="form-row">
<div class="field">
<Input label={$_('settings.newEmail')} placeholder={$_('settings.newEmailPlaceholder')} bind:value={inputValue} />
</div>
<div class="field">
<Input label={$_('security.passkeyName')} placeholder={$_('security.passkeyNamePlaceholder')} hint={$_('appPasswords.createdMessage')} />
</div>
</div>
<div class="form-row">
<div class="field">
<Input label={$_('verification.codeLabel')} placeholder={$_('verification.codePlaceholder')} error={$_('common.error')} bind:value={inputError} />
</div>
<div class="field">
<Input label={$_('settings.yourDomain')} placeholder={$_('settings.yourDomainPlaceholder')} disabled bind:value={inputDisabled} />
</div>
</div>
<div class="form-row mt-5">
<div class="field">
<label for="demo-select">{$_('settings.language')}</label>
<select id="demo-select">
{#each getSupportedLocales() as loc}
<option>{localeNames[loc]}</option>
{/each}
</select>
</div>
<div class="field">
<label for="demo-textarea">{$_('settings.exportData')}</label>
<textarea id="demo-textarea" rows="3"></textarea>
</div>
</div>
</Section>
<Section title="Cards">
<Card>
<h4>{$_('settings.exportData')}</h4>
<p class="text-secondary text-sm">{$_('settings.downloadRepo')}</p>
</Card>
<div class="mt-4">
<Card variant="interactive">
<h4>{$_('sessions.session')}</h4>
<p class="text-secondary text-sm">{$_('sessions.current')}</p>
</Card>
</div>
<div class="mt-4">
<Card variant="danger">
<h4>{$_('security.removePassword')}</h4>
<p class="text-secondary text-sm">{$_('security.removePasswordWarning')}</p>
</Card>
</div>
</Section>
<Section title="Sections">
<Section title="Default section" description="With a description">
<p>Section content</p>
</Section>
<div class="mt-5">
<Section title="Danger section" variant="danger">
<p>Destructive operations</p>
</Section>
</div>
</Section>
<Section title="Messages">
<Message variant="success">{$_('appPasswords.deleted')}</Message>
<div class="mt-4"><Message variant="error">{$_('appPasswords.createFailed')}</Message></div>
<div class="mt-4"><Message variant="warning">{$_('security.legacyLoginWarning')}</Message></div>
<div class="mt-4"><Message variant="info">{$_('appPasswords.createdMessage')}</Message></div>
</Section>
<Section title="Badges">
<p>
<span class="badge success">{$_('inviteCodes.available')}</span>
<span class="badge warning">{$_('inviteCodes.spent')}</span>
<span class="badge error">{$_('inviteCodes.disabled')}</span>
<span class="badge accent">{$_('sessions.current')}</span>
</p>
</Section>
<Section title="Toasts">
<p>
<Button variant="secondary" onclick={() => toast.success($_('appPasswords.deleted'))}>{$_('appPasswords.deleted')}</Button>
<Button variant="secondary" onclick={() => toast.error($_('appPasswords.createFailed'))}>{$_('appPasswords.createFailed')}</Button>
<Button variant="secondary" onclick={() => toast.warning($_('security.disableTotpWarning'))}>{$_('security.disableTotpWarning')}</Button>
<Button variant="secondary" onclick={() => toast.info($_('appPasswords.createdMessage'))}>{$_('appPasswords.createdMessage')}</Button>
</p>
</Section>
<Section title="Skeleton loading">
<Skeleton variant="line" size="full" />
<Skeleton variant="line" size="medium" />
<Skeleton variant="line" size="short" />
<Skeleton variant="line" size="tiny" />
<div class="mt-5">
<Skeleton variant="line" lines={3} />
</div>
<div class="mt-5">
<Skeleton variant="card" lines={2} />
</div>
</Section>
<Section title="Fieldset">
<fieldset>
<legend>Account settings</legend>
<div class="field">
<label for="demo-display-name">Display name</label>
<input id="demo-display-name" type="text" placeholder="Name" />
</div>
</fieldset>
</Section>
<Section title="Form layouts">
<h4>Two column</h4>
<div class="form-row">
<div class="field">
<label for="demo-fname">First name</label>
<input id="demo-fname" type="text" />
</div>
<div class="field">
<label for="demo-lname">Last name</label>
<input id="demo-lname" type="text" />
</div>
</div>
<h4 class="mt-5">Three column</h4>
<div class="form-row thirds">
<div class="field">
<label for="demo-city">City</label>
<input id="demo-city" type="text" />
</div>
<div class="field">
<label for="demo-state">State</label>
<input id="demo-state" type="text" />
</div>
<div class="field">
<label for="demo-zip">ZIP</label>
<input id="demo-zip" type="text" />
</div>
</div>
<h4 class="mt-5">Full width in row</h4>
<div class="form-row">
<div class="field">
<label for="demo-handle">Handle</label>
<input id="demo-handle" type="text" />
</div>
<div class="field">
<label for="demo-domain">Domain</label>
<select id="demo-domain"><option>example.com</option></select>
</div>
<div class="field full-width">
<label for="demo-bio">Bio</label>
<textarea id="demo-bio" rows="2"></textarea>
</div>
</div>
</Section>
<Section title="Hints">
<div class="field">
<label for="demo-hint">With hints</label>
<input id="demo-hint" type="text" />
<span class="hint">Default hint</span>
</div>
<div class="field">
<input type="text" />
<span class="hint warning">Warning hint</span>
</div>
<div class="field">
<input type="text" />
<span class="hint error">Error hint</span>
</div>
<div class="field">
<input type="text" />
<span class="hint success">Success hint</span>
</div>
</Section>
<Section title="Radio group">
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="demo-radio" checked />
<div class="radio-content">
<span>Option A</span>
<span class="radio-hint">First choice</span>
</div>
</label>
<label class="radio-label">
<input type="radio" name="demo-radio" />
<div class="radio-content">
<span>Option B</span>
<span class="radio-hint">Second choice</span>
</div>
</label>
<label class="radio-label disabled">
<input type="radio" name="demo-radio" disabled />
<div class="radio-content">
<span>Option C</span>
<span class="radio-hint disabled-hint">Unavailable</span>
</div>
</label>
</div>
</Section>
<Section title="Checkbox">
<label class="checkbox-label">
<input type="checkbox" />
<span>{$_('appPasswords.acknowledgeLabel')}</span>
</label>
</Section>
<Section title="Warning box">
<div class="warning-box">
<strong>{$_('appPasswords.saveWarningTitle')}</strong>
<p>{$_('appPasswords.saveWarningMessage')}</p>
</div>
</Section>
<Section title="Split layout">
<div class="split-layout">
<Card>
<h4>Main content</h4>
<p class="text-secondary">Primary area with a form or data</p>
<div class="field mt-5">
<label for="demo-example">Example field</label>
<input id="demo-example" type="text" placeholder="Value" />
</div>
</Card>
<div class="info-panel">
<h3>Sidebar</h3>
<p>Supplementary information placed alongside the main content area.</p>
<ul class="info-list">
<li>Supports DID methods: did:web, did:plc</li>
<li>Maximum blob size: 10MB</li>
<li>Rate limit: 100 requests per minute</li>
</ul>
</div>
</div>
</Section>
<Section title="Composite: card with form">
<Card>
<h4>{$_('appPasswords.create')}</h4>
<p class="text-secondary text-sm mb-5">{$_('appPasswords.permissions')}</p>
<div class="field">
<Input label={$_('appPasswords.name')} placeholder={$_('appPasswords.namePlaceholder')} />
</div>
<div class="mt-5" style="display: flex; gap: var(--space-3); justify-content: flex-end">
<Button variant="tertiary">{$_('common.cancel')}</Button>
<Button>{$_('appPasswords.create')}</Button>
</div>
</Card>
</Section>
<Section title="Composite: section with actions">
<Section title={$_('security.passkeys')}>
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<strong>{$_('security.totp')}</strong>
<p class="text-sm text-secondary">{$_('security.totpEnabled')}</p>
</div>
<Button variant="secondary" size="sm">{$_('security.disableTotp')}</Button>
</div>
<hr />
<div style="display: flex; justify-content: space-between; align-items: center">
<div>
<strong>{$_('security.passkeys')}</strong>
<p class="text-sm text-secondary">{$_('security.noPasskeys')}</p>
</div>
<Button variant="secondary" size="sm">{$_('security.addPasskey')}</Button>
</div>
</Section>
</Section>
<Section title="Composite: error state">
<div class="error-container">
<div class="error-icon">!</div>
<h2>Authorization failed</h2>
<p>The requested scope exceeds the granted permissions.</p>
<Button variant="secondary">Back</Button>
</div>
</Section>
<Section title="Item list">
<div class="item">
<div class="item-info">
<strong>Primary passkey</strong>
<span class="text-sm text-secondary">Created 2024-01-15</span>
</div>
<div class="item-actions">
<button class="sm">Rename</button>
<button class="sm danger-outline">Revoke</button>
</div>
</div>
<div class="item">
<div class="item-info">
<strong>Backup passkey</strong>
<span class="text-sm text-secondary">Created 2024-03-20</span>
</div>
<div class="item-actions">
<button class="sm">Rename</button>
<button class="sm danger-outline">Revoke</button>
</div>
</div>
<div class="item">
<div class="item-info">
<strong>Work laptop</strong>
<span class="text-sm text-secondary">Created 2024-06-01</span>
</div>
<div class="item-actions">
<button class="sm danger-outline">Revoke</button>
</div>
</div>
</Section>
<Section title="Definition list">
<dl class="definition-list">
<dt>Handle</dt>
<dd>@alice.example.com</dd>
<dt>DID</dt>
<dd class="mono">did:web:alice.example.com</dd>
<dt>Email</dt>
<dd>alice@example.com</dd>
<dt>Created</dt>
<dd>2024-01-15</dd>
<dt>Status</dt>
<dd><span class="badge success">Verified</span></dd>
</dl>
</Section>
<Section title="Tabs">
<div class="tabs">
<button class="tab active">PDS handle</button>
<button class="tab">Custom domain</button>
</div>
<p class="text-secondary">Tab content appears here</p>
</Section>
<Section title="Inline form">
<div class="inline-form">
<h4>Change password</h4>
<div class="field">
<label for="demo-current-pw">Current password</label>
<input id="demo-current-pw" type="password" />
</div>
<div class="field">
<label for="demo-new-pw">New password</label>
<input id="demo-new-pw" type="password" />
</div>
<div style="display: flex; gap: var(--space-3); justify-content: flex-end">
<Button variant="secondary">Cancel</Button>
<Button>Save</Button>
</div>
</div>
</Section>
</Page>