+534
Diff
round #0
+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(false)
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
1 round
0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
feat(frontend): add UI component test page
expand 0 comments
pull request successfully merged