-1084
Diff
round #2
-550
frontend/src/routes/RegisterPassword.svelte
-550
frontend/src/routes/RegisterPassword.svelte
···
1
-
<script lang="ts">
2
-
import { navigate, routes, getFullUrl } from '../lib/router.svelte'
3
-
import { api, ApiError } from '../lib/api'
4
-
import { _ } from '../lib/i18n'
5
-
import {
6
-
createRegistrationFlow,
7
-
restoreRegistrationFlow,
8
-
VerificationStep,
9
-
KeyChoiceStep,
10
-
DidDocStep,
11
-
} from '../lib/registration'
12
-
import AccountTypeSwitcher from '../components/AccountTypeSwitcher.svelte'
13
-
import HandleInput from '../components/HandleInput.svelte'
14
-
import { ensureRequestUri, getRequestUriFromUrl } from '../lib/oauth'
15
-
16
-
let serverInfo = $state<{
17
-
availableUserDomains: string[]
18
-
inviteCodeRequired: boolean
19
-
availableCommsChannels?: string[]
20
-
selfHostedDidWebEnabled?: boolean
21
-
} | null>(null)
22
-
let loadingServerInfo = $state(true)
23
-
let serverInfoLoaded = false
24
-
let ssoAvailable = $state(false)
25
-
26
-
let flow = $state<ReturnType<typeof createRegistrationFlow> | null>(null)
27
-
let confirmPassword = $state('')
28
-
let clientName = $state<string | null>(null)
29
-
let selectedDomain = $state('')
30
-
let checkHandleTimeout: ReturnType<typeof setTimeout> | null = null
31
-
32
-
$effect(() => {
33
-
if (!flow) return
34
-
const handle = flow.info.handle
35
-
if (checkHandleTimeout) {
36
-
clearTimeout(checkHandleTimeout)
37
-
}
38
-
if (handle.length >= 3 && !handle.includes('.')) {
39
-
checkHandleTimeout = setTimeout(() => flow?.checkHandleAvailability(handle), 400)
40
-
}
41
-
})
42
-
43
-
$effect(() => {
44
-
if (!serverInfoLoaded) {
45
-
serverInfoLoaded = true
46
-
ensureRequestUri().then((requestUri) => {
47
-
if (!requestUri) return
48
-
loadServerInfo()
49
-
checkSsoAvailable()
50
-
fetchClientName()
51
-
}).catch((err) => {
52
-
console.error('Failed to ensure OAuth request URI:', err)
53
-
})
54
-
}
55
-
})
56
-
57
-
async function fetchClientName() {
58
-
const requestUri = getRequestUriFromUrl()
59
-
if (!requestUri) return
60
-
61
-
try {
62
-
const response = await fetch(`/oauth/authorize?request_uri=${encodeURIComponent(requestUri)}`, {
63
-
headers: { 'Accept': 'application/json' }
64
-
})
65
-
if (response.ok) {
66
-
const data = await response.json()
67
-
clientName = data.client_name || null
68
-
}
69
-
} catch {
70
-
clientName = null
71
-
}
72
-
}
73
-
74
-
async function checkSsoAvailable() {
75
-
try {
76
-
const response = await fetch('/oauth/sso/providers')
77
-
if (response.ok) {
78
-
const data = await response.json()
79
-
ssoAvailable = (data.providers?.length ?? 0) > 0
80
-
}
81
-
} catch {
82
-
ssoAvailable = false
83
-
}
84
-
}
85
-
86
-
$effect(() => {
87
-
if (flow?.state.step === 'redirect-to-dashboard') {
88
-
completeOAuthRegistration()
89
-
}
90
-
})
91
-
92
-
let creatingStarted = false
93
-
$effect(() => {
94
-
if (flow?.state.step === 'creating' && !creatingStarted) {
95
-
creatingStarted = true
96
-
flow.createPasswordAccount()
97
-
}
98
-
})
99
-
100
-
async function loadServerInfo() {
101
-
try {
102
-
const restored = restoreRegistrationFlow()
103
-
if (restored && restored.state.mode === 'password') {
104
-
flow = restored
105
-
serverInfo = await api.describeServer()
106
-
} else {
107
-
serverInfo = await api.describeServer()
108
-
const hostname = serverInfo?.availableUserDomains?.[0] || window.location.hostname
109
-
flow = createRegistrationFlow('password', hostname)
110
-
}
111
-
selectedDomain = serverInfo?.availableUserDomains?.[0] || window.location.hostname
112
-
if (flow) flow.setSelectedDomain(selectedDomain)
113
-
} catch (e) {
114
-
console.error('Failed to load server info:', e)
115
-
} finally {
116
-
loadingServerInfo = false
117
-
}
118
-
}
119
-
120
-
function validateInfoStep(): string | null {
121
-
if (!flow) return 'Flow not initialized'
122
-
const info = flow.info
123
-
if (!info.handle.trim()) return $_('register.validation.handleRequired')
124
-
if (info.handle.includes('.')) return $_('register.validation.handleNoDots')
125
-
if (!info.password) return $_('register.validation.passwordRequired')
126
-
if (info.password.length < 8) return $_('register.validation.passwordLength')
127
-
if (info.password !== confirmPassword) return $_('register.validation.passwordsMismatch')
128
-
if (serverInfo?.inviteCodeRequired && !info.inviteCode?.trim()) {
129
-
return $_('register.validation.inviteCodeRequired')
130
-
}
131
-
if (info.didType === 'web-external') {
132
-
if (!info.externalDid?.trim()) return $_('register.validation.externalDidRequired')
133
-
if (!info.externalDid.trim().startsWith('did:web:')) return $_('register.validation.externalDidFormat')
134
-
}
135
-
switch (info.verificationChannel) {
136
-
case 'email':
137
-
if (!info.email.trim()) return $_('register.validation.emailRequired')
138
-
break
139
-
case 'discord':
140
-
if (!info.discordUsername?.trim()) return $_('register.validation.discordUsernameRequired')
141
-
break
142
-
case 'telegram':
143
-
if (!info.telegramUsername?.trim()) return $_('register.validation.telegramRequired')
144
-
break
145
-
case 'signal':
146
-
if (!info.signalUsername?.trim()) return $_('register.validation.signalRequired')
147
-
break
148
-
}
149
-
return null
150
-
}
151
-
152
-
async function handleInfoSubmit(e: Event) {
153
-
e.preventDefault()
154
-
if (!flow) return
155
-
156
-
const validationError = validateInfoStep()
157
-
if (validationError) {
158
-
flow.setError(validationError)
159
-
return
160
-
}
161
-
162
-
flow.clearError()
163
-
flow.proceedFromInfo()
164
-
}
165
-
166
-
async function handleCreateAccount() {
167
-
if (!flow) return
168
-
await flow.createPasswordAccount()
169
-
}
170
-
171
-
async function handleComplete() {
172
-
if (flow) {
173
-
await flow.finalizeSession()
174
-
}
175
-
navigate(routes.dashboard)
176
-
}
177
-
178
-
async function completeOAuthRegistration() {
179
-
const requestUri = getRequestUriFromUrl()
180
-
if (!requestUri || !flow?.account) {
181
-
navigate(routes.dashboard)
182
-
return
183
-
}
184
-
185
-
try {
186
-
const response = await fetch('/oauth/register/complete', {
187
-
method: 'POST',
188
-
headers: {
189
-
'Content-Type': 'application/json',
190
-
'Accept': 'application/json',
191
-
},
192
-
body: JSON.stringify({
193
-
request_uri: requestUri,
194
-
did: flow.account.did,
195
-
app_password: flow.account.appPassword || flow.info.password,
196
-
}),
197
-
})
198
-
199
-
const data = await response.json()
200
-
201
-
if (!response.ok) {
202
-
flow.setError(data.error_description || data.error || $_('common.error'))
203
-
return
204
-
}
205
-
206
-
if (data.redirect_uri) {
207
-
window.location.href = data.redirect_uri
208
-
return
209
-
}
210
-
211
-
navigate(routes.dashboard)
212
-
} catch (err) {
213
-
console.error('OAuth registration completion failed:', err)
214
-
flow.setError(err instanceof Error ? err.message : $_('common.error'))
215
-
}
216
-
}
217
-
218
-
function isChannelAvailable(ch: string): boolean {
219
-
const available = serverInfo?.availableCommsChannels ?? ['email']
220
-
return available.includes(ch)
221
-
}
222
-
223
-
function channelLabel(ch: string): string {
224
-
switch (ch) {
225
-
case 'email': return $_('register.email')
226
-
case 'discord': return $_('register.discord')
227
-
case 'telegram': return $_('register.telegram')
228
-
case 'signal': return $_('register.signal')
229
-
default: return ch
230
-
}
231
-
}
232
-
233
-
let fullHandle = $derived(() => {
234
-
if (!flow?.info.handle.trim()) return ''
235
-
if (flow.info.handle.includes('.')) return flow.info.handle.trim()
236
-
return selectedDomain ? `${flow.info.handle.trim()}.${selectedDomain}` : flow.info.handle.trim()
237
-
})
238
-
239
-
function extractDomain(did: string): string {
240
-
return did.replace('did:web:', '').replace(/%3A/g, ':')
241
-
}
242
-
243
-
async function handleCancel() {
244
-
const requestUri = getRequestUriFromUrl()
245
-
if (!requestUri) {
246
-
window.history.back()
247
-
return
248
-
}
249
-
250
-
try {
251
-
const response = await fetch('/oauth/authorize/deny', {
252
-
method: 'POST',
253
-
headers: {
254
-
'Content-Type': 'application/json',
255
-
'Accept': 'application/json'
256
-
},
257
-
body: JSON.stringify({ request_uri: requestUri })
258
-
})
259
-
260
-
if (!response.ok) {
261
-
window.history.back()
262
-
return
263
-
}
264
-
265
-
const data = await response.json()
266
-
if (data.redirect_uri) {
267
-
window.location.href = data.redirect_uri
268
-
} else {
269
-
window.history.back()
270
-
}
271
-
} catch {
272
-
window.history.back()
273
-
}
274
-
}
275
-
</script>
276
-
277
-
<div class="page">
278
-
<header class="page-header">
279
-
<h1>{$_('register.title')}</h1>
280
-
{#if clientName}
281
-
<p class="subtitle">{$_('oauth.register.subtitle')} <strong>{clientName}</strong></p>
282
-
{/if}
283
-
</header>
284
-
285
-
{#if flow?.state.error}
286
-
<div class="message error">{flow.state.error}</div>
287
-
{/if}
288
-
289
-
{#if loadingServerInfo || !flow}
290
-
<div class="loading"></div>
291
-
{:else if flow.state.step === 'info'}
292
-
<div class="migrate-callout">
293
-
<div class="migrate-icon">↗</div>
294
-
<div class="migrate-content">
295
-
<strong>{$_('register.migrateTitle')}</strong>
296
-
<p>{$_('register.migrateDescription')}</p>
297
-
<a href={getFullUrl(routes.migrate)} class="migrate-link">
298
-
{$_('register.migrateLink')} →
299
-
</a>
300
-
</div>
301
-
</div>
302
-
303
-
<AccountTypeSwitcher active="password" {ssoAvailable} oauthRequestUri={getRequestUriFromUrl()} />
304
-
305
-
<form class="register-form" onsubmit={handleInfoSubmit}>
306
-
<div>
307
-
<label for="handle">{$_('register.handle')}</label>
308
-
<HandleInput
309
-
value={flow.info.handle}
310
-
domains={serverInfo?.availableUserDomains ?? []}
311
-
{selectedDomain}
312
-
placeholder={$_('register.handlePlaceholder')}
313
-
disabled={flow.state.submitting}
314
-
onInput={(v) => { flow!.info.handle = v }}
315
-
onDomainChange={(d) => { selectedDomain = d; flow!.setSelectedDomain(d) }}
316
-
/>
317
-
{#if flow.info.handle.includes('.')}
318
-
<p class="hint warning">{$_('register.handleDotWarning')}</p>
319
-
{:else if flow.state.checkingHandle}
320
-
<p class="hint">{$_('common.checking')}</p>
321
-
{:else if flow.state.handleAvailable === false}
322
-
<p class="hint warning">{$_('register.handleTaken')}</p>
323
-
{:else if flow.state.handleAvailable === true && fullHandle()}
324
-
<p class="hint success">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
325
-
{:else if fullHandle()}
326
-
<p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p>
327
-
{/if}
328
-
</div>
329
-
330
-
<div>
331
-
<label for="password">{$_('register.password')}</label>
332
-
<input
333
-
id="password"
334
-
type="password"
335
-
bind:value={flow.info.password}
336
-
placeholder={$_('register.passwordPlaceholder')}
337
-
disabled={flow.state.submitting}
338
-
required
339
-
minlength="8"
340
-
/>
341
-
</div>
342
-
343
-
<div>
344
-
<label for="confirm-password">{$_('register.confirmPassword')}</label>
345
-
<input
346
-
id="confirm-password"
347
-
type="password"
348
-
bind:value={confirmPassword}
349
-
placeholder={$_('register.confirmPasswordPlaceholder')}
350
-
disabled={flow.state.submitting}
351
-
required
352
-
/>
353
-
</div>
354
-
355
-
<div>
356
-
<label for="verification-channel">{$_('register.verificationMethod')}</label>
357
-
<select id="verification-channel" bind:value={flow.info.verificationChannel} disabled={flow.state.submitting}>
358
-
<option value="email">{$_('register.email')}</option>
359
-
{#if isChannelAvailable('discord')}
360
-
<option value="discord">{$_('register.discord')}</option>
361
-
{/if}
362
-
{#if isChannelAvailable('telegram')}
363
-
<option value="telegram">{$_('register.telegram')}</option>
364
-
{/if}
365
-
{#if isChannelAvailable('signal')}
366
-
<option value="signal">{$_('register.signal')}</option>
367
-
{/if}
368
-
</select>
369
-
</div>
370
-
371
-
{#if flow.info.verificationChannel === 'email'}
372
-
<div>
373
-
<label for="email">{$_('register.emailAddress')}</label>
374
-
<input
375
-
id="email"
376
-
type="email"
377
-
bind:value={flow.info.email}
378
-
placeholder={$_('register.emailPlaceholder')}
379
-
disabled={flow.state.submitting}
380
-
required
381
-
/>
382
-
</div>
383
-
{:else if flow.info.verificationChannel === 'discord'}
384
-
<div>
385
-
<label for="discord-username">{$_('register.discordUsername')}</label>
386
-
<input
387
-
id="discord-username"
388
-
type="text"
389
-
bind:value={flow.info.discordUsername}
390
-
onblur={() => flow?.checkCommsChannelInUse('discord', flow.info.discordUsername ?? '')}
391
-
placeholder={$_('register.discordUsernamePlaceholder')}
392
-
disabled={flow.state.submitting}
393
-
required
394
-
/>
395
-
{#if flow.state.discordInUse}
396
-
<p class="hint warning">{$_('register.discordInUseWarning')}</p>
397
-
{/if}
398
-
</div>
399
-
{:else if flow.info.verificationChannel === 'telegram'}
400
-
<div>
401
-
<label for="telegram-username">{$_('register.telegramUsername')}</label>
402
-
<input
403
-
id="telegram-username"
404
-
type="text"
405
-
bind:value={flow.info.telegramUsername}
406
-
onblur={() => flow?.checkCommsChannelInUse('telegram', flow.info.telegramUsername ?? '')}
407
-
placeholder={$_('register.telegramUsernamePlaceholder')}
408
-
disabled={flow.state.submitting}
409
-
required
410
-
/>
411
-
{#if flow.state.telegramInUse}
412
-
<p class="hint warning">{$_('register.telegramInUseWarning')}</p>
413
-
{/if}
414
-
</div>
415
-
{:else if flow.info.verificationChannel === 'signal'}
416
-
<div>
417
-
<label for="signal-number">{$_('register.signalUsername')}</label>
418
-
<input
419
-
id="signal-number"
420
-
type="tel"
421
-
bind:value={flow.info.signalUsername}
422
-
onblur={() => flow?.checkCommsChannelInUse('signal', flow.info.signalUsername ?? '')}
423
-
placeholder={$_('register.signalUsernamePlaceholder')}
424
-
disabled={flow.state.submitting}
425
-
required
426
-
/>
427
-
<p class="hint">{$_('register.signalUsernameHint')}</p>
428
-
{#if flow.state.signalInUse}
429
-
<p class="hint warning">{$_('register.signalInUseWarning')}</p>
430
-
{/if}
431
-
</div>
432
-
{/if}
433
-
434
-
<fieldset class="identity-section">
435
-
<legend>{$_('register.identityType')}</legend>
436
-
<div class="radio-group">
437
-
<label class="radio-label">
438
-
<input type="radio" name="didType" value="plc" bind:group={flow.info.didType} disabled={flow.state.submitting} />
439
-
<span class="radio-content">
440
-
<strong>{$_('register.didPlc')}</strong>
441
-
<span class="radio-hint">{$_('register.didPlcHint')}</span>
442
-
</span>
443
-
</label>
444
-
445
-
<label class="radio-label" class:disabled={serverInfo?.selfHostedDidWebEnabled === false}>
446
-
<input type="radio" name="didType" value="web" bind:group={flow.info.didType} disabled={flow.state.submitting || serverInfo?.selfHostedDidWebEnabled === false} />
447
-
<span class="radio-content">
448
-
<strong>{$_('register.didWeb')}</strong>
449
-
{#if serverInfo?.selfHostedDidWebEnabled === false}
450
-
<span class="radio-hint disabled-hint">{$_('register.didWebDisabledHint')}</span>
451
-
{:else}
452
-
<span class="radio-hint">{$_('register.didWebHint')}</span>
453
-
{/if}
454
-
</span>
455
-
</label>
456
-
457
-
<label class="radio-label">
458
-
<input type="radio" name="didType" value="web-external" bind:group={flow.info.didType} disabled={flow.state.submitting} />
459
-
<span class="radio-content">
460
-
<strong>{$_('register.didWebBYOD')}</strong>
461
-
<span class="radio-hint">{$_('register.didWebBYODHint')}</span>
462
-
</span>
463
-
</label>
464
-
</div>
465
-
</fieldset>
466
-
467
-
{#if flow.info.didType === 'web'}
468
-
<div class="warning-box">
469
-
<strong>{$_('register.didWebWarningTitle')}</strong>
470
-
<ul>
471
-
<li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li>
472
-
<li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li>
473
-
{#if $_('register.didWebWarning3')}
474
-
<li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li>
475
-
{/if}
476
-
</ul>
477
-
</div>
478
-
{/if}
479
-
480
-
{#if flow.info.didType === 'web-external'}
481
-
<div>
482
-
<label for="external-did">{$_('register.externalDid')}</label>
483
-
<input
484
-
id="external-did"
485
-
type="text"
486
-
bind:value={flow.info.externalDid}
487
-
placeholder={$_('register.externalDidPlaceholder')}
488
-
disabled={flow.state.submitting}
489
-
required
490
-
/>
491
-
<p class="hint">{$_('register.externalDidHint')}</p>
492
-
</div>
493
-
{/if}
494
-
495
-
{#if serverInfo?.inviteCodeRequired}
496
-
<div>
497
-
<label for="invite-code">{$_('register.inviteCode')}</label>
498
-
<input
499
-
id="invite-code"
500
-
type="text"
501
-
bind:value={flow.info.inviteCode}
502
-
placeholder={$_('register.inviteCodePlaceholder')}
503
-
disabled={flow.state.submitting}
504
-
required
505
-
/>
506
-
</div>
507
-
{/if}
508
-
509
-
<div class="form-actions">
510
-
<button type="button" class="secondary" onclick={handleCancel} disabled={flow.state.submitting}>
511
-
{$_('common.cancel')}
512
-
</button>
513
-
<button type="submit" class="primary" disabled={flow.state.submitting || flow.state.handleAvailable === false || flow.state.checkingHandle}>
514
-
{flow.state.submitting ? $_('common.loading') : $_('common.continue')}
515
-
</button>
516
-
</div>
517
-
</form>
518
-
519
-
{:else if flow.state.step === 'key-choice'}
520
-
<KeyChoiceStep {flow} />
521
-
522
-
{:else if flow.state.step === 'initial-did-doc'}
523
-
<DidDocStep
524
-
{flow}
525
-
type="initial"
526
-
onConfirm={handleCreateAccount}
527
-
onBack={() => flow?.goBack()}
528
-
/>
529
-
530
-
{:else if flow.state.step === 'creating'}
531
-
<div class="loading">
532
-
<p>{$_('common.creating')}</p>
533
-
</div>
534
-
535
-
{:else if flow.state.step === 'verify'}
536
-
<VerificationStep {flow} />
537
-
538
-
{:else if flow.state.step === 'updated-did-doc'}
539
-
<DidDocStep
540
-
{flow}
541
-
type="updated"
542
-
onConfirm={() => flow?.activateAccount()}
543
-
/>
544
-
545
-
{:else if flow.state.step === 'redirect-to-dashboard'}
546
-
<div class="loading">
547
-
<p>{$_('register.redirecting')}</p>
548
-
</div>
549
-
{/if}
550
-
</div>
-534
frontend/src/routes/UiTest.svelte
-534
frontend/src/routes/UiTest.svelte
···
1
-
<script lang="ts">
2
-
import { Button, Card, Input, Message, Page, Section } from '../components/ui'
3
-
import Skeleton from '../components/Skeleton.svelte'
4
-
import { toast } from '../lib/toast.svelte'
5
-
import { getServerConfigState } from '../lib/serverConfig.svelte'
6
-
import { _, locale, getSupportedLocales, localeNames, type SupportedLocale } from '../lib/i18n'
7
-
8
-
let inputValue = $state('')
9
-
let inputError = $state('')
10
-
let inputDisabled = $state('')
11
-
12
-
const serverConfig = getServerConfigState()
13
-
14
-
const LIGHT_ACCENT_DEFAULT = '#1a1d1d'
15
-
const DARK_ACCENT_DEFAULT = '#e6e8e8'
16
-
const LIGHT_SECONDARY_DEFAULT = '#1a1d1d'
17
-
const DARK_SECONDARY_DEFAULT = '#e6e8e8'
18
-
19
-
let accentLight = $state(LIGHT_ACCENT_DEFAULT)
20
-
let accentDark = $state(DARK_ACCENT_DEFAULT)
21
-
let secondaryLight = $state(LIGHT_SECONDARY_DEFAULT)
22
-
let secondaryDark = $state(DARK_SECONDARY_DEFAULT)
23
-
24
-
$effect(() => {
25
-
accentLight = serverConfig.primaryColor || LIGHT_ACCENT_DEFAULT
26
-
accentDark = serverConfig.primaryColorDark || DARK_ACCENT_DEFAULT
27
-
secondaryLight = serverConfig.secondaryColor || LIGHT_SECONDARY_DEFAULT
28
-
secondaryDark = serverConfig.secondaryColorDark || DARK_SECONDARY_DEFAULT
29
-
})
30
-
31
-
const isDark = $derived(
32
-
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
33
-
)
34
-
35
-
function applyColor(prop: string, value: string): void {
36
-
document.documentElement.style.setProperty(prop, value)
37
-
}
38
-
39
-
$effect(() => {
40
-
applyColor('--accent', isDark ? accentDark : accentLight)
41
-
})
42
-
43
-
$effect(() => {
44
-
applyColor('--secondary', isDark ? secondaryDark : secondaryLight)
45
-
})
46
-
</script>
47
-
48
-
<Page title="UI Test" size="lg">
49
-
<Section title="Theme">
50
-
<div class="form-row">
51
-
<div class="field">
52
-
<label for="accent-light">Accent (light)</label>
53
-
<div class="color-pair">
54
-
<input type="color" bind:value={accentLight} />
55
-
<input id="accent-light" type="text" class="mono" bind:value={accentLight} />
56
-
</div>
57
-
</div>
58
-
<div class="field">
59
-
<label for="accent-dark">Accent (dark)</label>
60
-
<div class="color-pair">
61
-
<input type="color" bind:value={accentDark} />
62
-
<input id="accent-dark" type="text" class="mono" bind:value={accentDark} />
63
-
</div>
64
-
</div>
65
-
<div class="field">
66
-
<label for="secondary-light">Secondary (light)</label>
67
-
<div class="color-pair">
68
-
<input type="color" bind:value={secondaryLight} />
69
-
<input id="secondary-light" type="text" class="mono" bind:value={secondaryLight} />
70
-
</div>
71
-
</div>
72
-
<div class="field">
73
-
<label for="secondary-dark">Secondary (dark)</label>
74
-
<div class="color-pair">
75
-
<input type="color" bind:value={secondaryDark} />
76
-
<input id="secondary-dark" type="text" class="mono" bind:value={secondaryDark} />
77
-
</div>
78
-
</div>
79
-
<div class="field">
80
-
<label for="locale-picker">Locale</label>
81
-
<select id="locale-picker" value={$locale} onchange={(e) => locale.set(e.currentTarget.value)}>
82
-
{#each getSupportedLocales() as loc}
83
-
<option value={loc}>{localeNames[loc]} ({loc})</option>
84
-
{/each}
85
-
</select>
86
-
</div>
87
-
</div>
88
-
</Section>
89
-
90
-
<Section title="Typography">
91
-
<p style="font-size: var(--text-4xl)">4xl (2.5rem)</p>
92
-
<p style="font-size: var(--text-3xl)">3xl (2rem)</p>
93
-
<p style="font-size: var(--text-2xl)">2xl (1.5rem)</p>
94
-
<p style="font-size: var(--text-xl)">xl (1.25rem)</p>
95
-
<p style="font-size: var(--text-lg)">lg (1.125rem)</p>
96
-
<p style="font-size: var(--text-base)">base (1rem)</p>
97
-
<p style="font-size: var(--text-sm)">sm (0.875rem)</p>
98
-
<p style="font-size: var(--text-xs)">xs (0.75rem)</p>
99
-
<hr />
100
-
<p style="font-weight: var(--font-normal)">Normal (400)</p>
101
-
<p style="font-weight: var(--font-medium)">Medium (500)</p>
102
-
<p style="font-weight: var(--font-semibold)">Semibold (600)</p>
103
-
<p style="font-weight: var(--font-bold)">Bold (700)</p>
104
-
<hr />
105
-
<code>Monospace text</code>
106
-
<pre>Pre block
107
-
indented</pre>
108
-
</Section>
109
-
110
-
<Section title="Colors">
111
-
<div class="form-row">
112
-
<div>
113
-
<h4>Backgrounds</h4>
114
-
<div class="swatch" style="background: var(--bg-primary)">bg-primary</div>
115
-
<div class="swatch" style="background: var(--bg-secondary)">bg-secondary</div>
116
-
<div class="swatch" style="background: var(--bg-tertiary)">bg-tertiary</div>
117
-
<div class="swatch" style="background: var(--bg-card)">bg-card</div>
118
-
<div class="swatch" style="background: var(--bg-input)">bg-input</div>
119
-
</div>
120
-
<div>
121
-
<h4>Text</h4>
122
-
<p style="color: var(--text-primary)">text-primary</p>
123
-
<p style="color: var(--text-secondary)">text-secondary</p>
124
-
<p style="color: var(--text-muted)">text-muted</p>
125
-
<div class="swatch" style="background: var(--accent); color: var(--text-inverse)">text-inverse</div>
126
-
</div>
127
-
<div>
128
-
<h4>Accent</h4>
129
-
<div class="swatch" style="background: var(--accent); color: var(--text-inverse)">accent</div>
130
-
<div class="swatch" style="background: var(--accent-hover); color: var(--text-inverse)">accent-hover</div>
131
-
<div class="swatch" style="background: var(--accent-muted)">accent-muted</div>
132
-
</div>
133
-
<div>
134
-
<h4>Status</h4>
135
-
<div class="swatch" style="background: var(--success-bg); color: var(--success-text)">success</div>
136
-
<div class="swatch" style="background: var(--error-bg); color: var(--error-text)">error</div>
137
-
<div class="swatch" style="background: var(--warning-bg); color: var(--warning-text)">warning</div>
138
-
</div>
139
-
</div>
140
-
</Section>
141
-
142
-
<Section title="Spacing">
143
-
<div class="spacing-row">
144
-
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as i}
145
-
<div class="spacing-item">
146
-
<div class="spacing-box" style="width: var(--space-{i}); height: var(--space-{i})"></div>
147
-
<span class="text-xs text-muted">--space-{i}</span>
148
-
</div>
149
-
{/each}
150
-
</div>
151
-
</Section>
152
-
153
-
<Section title="Buttons">
154
-
<p>
155
-
<Button variant="primary">{$_('common.save')}</Button>
156
-
<Button variant="secondary">{$_('common.cancel')}</Button>
157
-
<Button variant="tertiary">{$_('common.back')}</Button>
158
-
<Button variant="danger">{$_('common.delete')}</Button>
159
-
<Button variant="ghost">{$_('common.refresh')}</Button>
160
-
</p>
161
-
<p class="mt-5">
162
-
<Button size="sm">{$_('common.verify')}</Button>
163
-
<Button size="md">{$_('common.continue')}</Button>
164
-
<Button size="lg">{$_('common.signIn')}</Button>
165
-
</p>
166
-
<p class="mt-5">
167
-
<Button disabled>{$_('common.save')}</Button>
168
-
<Button loading>{$_('common.saving')}</Button>
169
-
<Button variant="secondary" disabled>{$_('common.cancel')}</Button>
170
-
<Button variant="danger" loading>{$_('common.delete')}</Button>
171
-
</p>
172
-
<p class="mt-5">
173
-
<button class="danger-outline">{$_('common.revoke')}</button>
174
-
<button class="link">{$_('login.forgotPassword')}</button>
175
-
<button class="sm">{$_('common.done')}</button>
176
-
</p>
177
-
<div class="mt-5">
178
-
<Button fullWidth>{$_('common.signIn')}</Button>
179
-
</div>
180
-
</Section>
181
-
182
-
<Section title="Inputs">
183
-
<div class="form-row">
184
-
<div class="field">
185
-
<Input label={$_('settings.newEmail')} placeholder={$_('settings.newEmailPlaceholder')} bind:value={inputValue} />
186
-
</div>
187
-
<div class="field">
188
-
<Input label={$_('security.passkeyName')} placeholder={$_('security.passkeyNamePlaceholder')} hint={$_('appPasswords.createdMessage')} />
189
-
</div>
190
-
</div>
191
-
<div class="form-row">
192
-
<div class="field">
193
-
<Input label={$_('verification.codeLabel')} placeholder={$_('verification.codePlaceholder')} error={$_('common.error')} bind:value={inputError} />
194
-
</div>
195
-
<div class="field">
196
-
<Input label={$_('settings.yourDomain')} placeholder={$_('settings.yourDomainPlaceholder')} disabled bind:value={inputDisabled} />
197
-
</div>
198
-
</div>
199
-
<div class="form-row mt-5">
200
-
<div class="field">
201
-
<label for="demo-select">{$_('settings.language')}</label>
202
-
<select id="demo-select">
203
-
{#each getSupportedLocales() as loc}
204
-
<option>{localeNames[loc]}</option>
205
-
{/each}
206
-
</select>
207
-
</div>
208
-
<div class="field">
209
-
<label for="demo-textarea">{$_('settings.exportData')}</label>
210
-
<textarea id="demo-textarea" rows="3"></textarea>
211
-
</div>
212
-
</div>
213
-
</Section>
214
-
215
-
<Section title="Cards">
216
-
<Card>
217
-
<h4>{$_('settings.exportData')}</h4>
218
-
<p class="text-secondary text-sm">{$_('settings.downloadRepo')}</p>
219
-
</Card>
220
-
<div class="mt-4">
221
-
<Card variant="interactive">
222
-
<h4>{$_('sessions.session')}</h4>
223
-
<p class="text-secondary text-sm">{$_('sessions.current')}</p>
224
-
</Card>
225
-
</div>
226
-
<div class="mt-4">
227
-
<Card variant="danger">
228
-
<h4>{$_('security.removePassword')}</h4>
229
-
<p class="text-secondary text-sm">{$_('security.removePasswordWarning')}</p>
230
-
</Card>
231
-
</div>
232
-
</Section>
233
-
234
-
<Section title="Sections">
235
-
<Section title="Default section" description="With a description">
236
-
<p>Section content</p>
237
-
</Section>
238
-
<div class="mt-5">
239
-
<Section title="Danger section" variant="danger">
240
-
<p>Destructive operations</p>
241
-
</Section>
242
-
</div>
243
-
</Section>
244
-
245
-
<Section title="Messages">
246
-
<Message variant="success">{$_('appPasswords.deleted')}</Message>
247
-
<div class="mt-4"><Message variant="error">{$_('appPasswords.createFailed')}</Message></div>
248
-
<div class="mt-4"><Message variant="warning">{$_('security.legacyLoginWarning')}</Message></div>
249
-
<div class="mt-4"><Message variant="info">{$_('appPasswords.createdMessage')}</Message></div>
250
-
</Section>
251
-
252
-
<Section title="Badges">
253
-
<p>
254
-
<span class="badge success">{$_('inviteCodes.available')}</span>
255
-
<span class="badge warning">{$_('inviteCodes.spent')}</span>
256
-
<span class="badge error">{$_('inviteCodes.disabled')}</span>
257
-
<span class="badge accent">{$_('sessions.current')}</span>
258
-
</p>
259
-
</Section>
260
-
261
-
<Section title="Toasts">
262
-
<p>
263
-
<Button variant="secondary" onclick={() => toast.success($_('appPasswords.deleted'))}>{$_('appPasswords.deleted')}</Button>
264
-
<Button variant="secondary" onclick={() => toast.error($_('appPasswords.createFailed'))}>{$_('appPasswords.createFailed')}</Button>
265
-
<Button variant="secondary" onclick={() => toast.warning($_('security.disableTotpWarning'))}>{$_('security.disableTotpWarning')}</Button>
266
-
<Button variant="secondary" onclick={() => toast.info($_('appPasswords.createdMessage'))}>{$_('appPasswords.createdMessage')}</Button>
267
-
</p>
268
-
</Section>
269
-
270
-
<Section title="Skeleton loading">
271
-
<Skeleton variant="line" size="full" />
272
-
<Skeleton variant="line" size="medium" />
273
-
<Skeleton variant="line" size="short" />
274
-
<Skeleton variant="line" size="tiny" />
275
-
<div class="mt-5">
276
-
<Skeleton variant="line" lines={3} />
277
-
</div>
278
-
<div class="mt-5">
279
-
<Skeleton variant="card" lines={2} />
280
-
</div>
281
-
</Section>
282
-
283
-
<Section title="Fieldset">
284
-
<fieldset>
285
-
<legend>Account settings</legend>
286
-
<div class="field">
287
-
<label for="demo-display-name">Display name</label>
288
-
<input id="demo-display-name" type="text" placeholder="Name" />
289
-
</div>
290
-
</fieldset>
291
-
</Section>
292
-
293
-
<Section title="Form layouts">
294
-
<h4>Two column</h4>
295
-
<div class="form-row">
296
-
<div class="field">
297
-
<label for="demo-fname">First name</label>
298
-
<input id="demo-fname" type="text" />
299
-
</div>
300
-
<div class="field">
301
-
<label for="demo-lname">Last name</label>
302
-
<input id="demo-lname" type="text" />
303
-
</div>
304
-
</div>
305
-
<h4 class="mt-5">Three column</h4>
306
-
<div class="form-row thirds">
307
-
<div class="field">
308
-
<label for="demo-city">City</label>
309
-
<input id="demo-city" type="text" />
310
-
</div>
311
-
<div class="field">
312
-
<label for="demo-state">State</label>
313
-
<input id="demo-state" type="text" />
314
-
</div>
315
-
<div class="field">
316
-
<label for="demo-zip">ZIP</label>
317
-
<input id="demo-zip" type="text" />
318
-
</div>
319
-
</div>
320
-
<h4 class="mt-5">Full width in row</h4>
321
-
<div class="form-row">
322
-
<div class="field">
323
-
<label for="demo-handle">Handle</label>
324
-
<input id="demo-handle" type="text" />
325
-
</div>
326
-
<div class="field">
327
-
<label for="demo-domain">Domain</label>
328
-
<select id="demo-domain"><option>example.com</option></select>
329
-
</div>
330
-
<div class="field full-width">
331
-
<label for="demo-bio">Bio</label>
332
-
<textarea id="demo-bio" rows="2"></textarea>
333
-
</div>
334
-
</div>
335
-
</Section>
336
-
337
-
<Section title="Hints">
338
-
<div class="field">
339
-
<label for="demo-hint">With hints</label>
340
-
<input id="demo-hint" type="text" />
341
-
<span class="hint">Default hint</span>
342
-
</div>
343
-
<div class="field">
344
-
<input type="text" />
345
-
<span class="hint warning">Warning hint</span>
346
-
</div>
347
-
<div class="field">
348
-
<input type="text" />
349
-
<span class="hint error">Error hint</span>
350
-
</div>
351
-
<div class="field">
352
-
<input type="text" />
353
-
<span class="hint success">Success hint</span>
354
-
</div>
355
-
</Section>
356
-
357
-
<Section title="Radio group">
358
-
<div class="radio-group">
359
-
<label class="radio-label">
360
-
<input type="radio" name="demo-radio" checked />
361
-
<div class="radio-content">
362
-
<span>Option A</span>
363
-
<span class="radio-hint">First choice</span>
364
-
</div>
365
-
</label>
366
-
<label class="radio-label">
367
-
<input type="radio" name="demo-radio" />
368
-
<div class="radio-content">
369
-
<span>Option B</span>
370
-
<span class="radio-hint">Second choice</span>
371
-
</div>
372
-
</label>
373
-
<label class="radio-label disabled">
374
-
<input type="radio" name="demo-radio" disabled />
375
-
<div class="radio-content">
376
-
<span>Option C</span>
377
-
<span class="radio-hint disabled-hint">Unavailable</span>
378
-
</div>
379
-
</label>
380
-
</div>
381
-
</Section>
382
-
383
-
<Section title="Checkbox">
384
-
<label class="checkbox-label">
385
-
<input type="checkbox" />
386
-
<span>{$_('appPasswords.acknowledgeLabel')}</span>
387
-
</label>
388
-
</Section>
389
-
390
-
<Section title="Warning box">
391
-
<div class="warning-box">
392
-
<strong>{$_('appPasswords.saveWarningTitle')}</strong>
393
-
<p>{$_('appPasswords.saveWarningMessage')}</p>
394
-
</div>
395
-
</Section>
396
-
397
-
<Section title="Split layout">
398
-
<div class="split-layout">
399
-
<Card>
400
-
<h4>Main content</h4>
401
-
<p class="text-secondary">Primary area with a form or data</p>
402
-
<div class="field mt-5">
403
-
<label for="demo-example">Example field</label>
404
-
<input id="demo-example" type="text" placeholder="Value" />
405
-
</div>
406
-
</Card>
407
-
<div class="info-panel">
408
-
<h3>Sidebar</h3>
409
-
<p>Supplementary information placed alongside the main content area.</p>
410
-
<ul class="info-list">
411
-
<li>Supports DID methods: did:web, did:plc</li>
412
-
<li>Maximum blob size: 10MB</li>
413
-
<li>Rate limit: 100 requests per minute</li>
414
-
</ul>
415
-
</div>
416
-
</div>
417
-
</Section>
418
-
419
-
<Section title="Composite: card with form">
420
-
<Card>
421
-
<h4>{$_('appPasswords.create')}</h4>
422
-
<p class="text-secondary text-sm mb-5">{$_('appPasswords.permissions')}</p>
423
-
<div class="field">
424
-
<Input label={$_('appPasswords.name')} placeholder={$_('appPasswords.namePlaceholder')} />
425
-
</div>
426
-
<div class="mt-5" style="display: flex; gap: var(--space-3); justify-content: flex-end">
427
-
<Button variant="tertiary">{$_('common.cancel')}</Button>
428
-
<Button>{$_('appPasswords.create')}</Button>
429
-
</div>
430
-
</Card>
431
-
</Section>
432
-
433
-
<Section title="Composite: section with actions">
434
-
<Section title={$_('security.passkeys')}>
435
-
<div style="display: flex; justify-content: space-between; align-items: center">
436
-
<div>
437
-
<strong>{$_('security.totp')}</strong>
438
-
<p class="text-sm text-secondary">{$_('security.totpEnabled')}</p>
439
-
</div>
440
-
<Button variant="secondary" size="sm">{$_('security.disableTotp')}</Button>
441
-
</div>
442
-
<hr />
443
-
<div style="display: flex; justify-content: space-between; align-items: center">
444
-
<div>
445
-
<strong>{$_('security.passkeys')}</strong>
446
-
<p class="text-sm text-secondary">{$_('security.noPasskeys')}</p>
447
-
</div>
448
-
<Button variant="secondary" size="sm">{$_('security.addPasskey')}</Button>
449
-
</div>
450
-
</Section>
451
-
</Section>
452
-
453
-
<Section title="Composite: error state">
454
-
<div class="error-container">
455
-
<div class="error-icon">!</div>
456
-
<h2>Authorization failed</h2>
457
-
<p>The requested scope exceeds the granted permissions.</p>
458
-
<Button variant="secondary">Back</Button>
459
-
</div>
460
-
</Section>
461
-
462
-
<Section title="Item list">
463
-
<div class="item">
464
-
<div class="item-info">
465
-
<strong>Primary passkey</strong>
466
-
<span class="text-sm text-secondary">Created 2024-01-15</span>
467
-
</div>
468
-
<div class="item-actions">
469
-
<button class="sm">Rename</button>
470
-
<button class="sm danger-outline">Revoke</button>
471
-
</div>
472
-
</div>
473
-
<div class="item">
474
-
<div class="item-info">
475
-
<strong>Backup passkey</strong>
476
-
<span class="text-sm text-secondary">Created 2024-03-20</span>
477
-
</div>
478
-
<div class="item-actions">
479
-
<button class="sm">Rename</button>
480
-
<button class="sm danger-outline">Revoke</button>
481
-
</div>
482
-
</div>
483
-
<div class="item">
484
-
<div class="item-info">
485
-
<strong>Work laptop</strong>
486
-
<span class="text-sm text-secondary">Created 2024-06-01</span>
487
-
</div>
488
-
<div class="item-actions">
489
-
<button class="sm danger-outline">Revoke</button>
490
-
</div>
491
-
</div>
492
-
</Section>
493
-
494
-
<Section title="Definition list">
495
-
<dl class="definition-list">
496
-
<dt>Handle</dt>
497
-
<dd>@alice.example.com</dd>
498
-
<dt>DID</dt>
499
-
<dd class="mono">did:web:alice.example.com</dd>
500
-
<dt>Email</dt>
501
-
<dd>alice@example.com</dd>
502
-
<dt>Created</dt>
503
-
<dd>2024-01-15</dd>
504
-
<dt>Status</dt>
505
-
<dd><span class="badge success">Verified</span></dd>
506
-
</dl>
507
-
</Section>
508
-
509
-
<Section title="Tabs">
510
-
<div class="tabs">
511
-
<button class="tab active">PDS handle</button>
512
-
<button class="tab">Custom domain</button>
513
-
</div>
514
-
<p class="text-secondary">Tab content appears here</p>
515
-
</Section>
516
-
517
-
<Section title="Inline form">
518
-
<div class="inline-form">
519
-
<h4>Change password</h4>
520
-
<div class="field">
521
-
<label for="demo-current-pw">Current password</label>
522
-
<input id="demo-current-pw" type="password" />
523
-
</div>
524
-
<div class="field">
525
-
<label for="demo-new-pw">New password</label>
526
-
<input id="demo-new-pw" type="password" />
527
-
</div>
528
-
<div style="display: flex; gap: var(--space-3); justify-content: flex-end">
529
-
<Button variant="secondary">Cancel</Button>
530
-
<Button>Save</Button>
531
-
</div>
532
-
</div>
533
-
</Section>
534
-
</Page>
History
3 rounds
0 comments
oyster.cafe
submitted
#2
1 commit
expand
collapse
refactor(frontend): delete RegisterPassword and UiTest routes
expand 0 comments
pull request successfully merged
oyster.cafe
submitted
#1
1 commit
expand
collapse
refactor(frontend): delete RegisterPassword and UiTest routes
expand 0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
refactor(frontend): delete RegisterPassword and UiTest routes