refactor(frontend): extract oauth primary flow page styles

This commit is contained in:
Lewis
2026-03-18 07:18:08 +02:00
committed by Tangled
parent 14bac09cb1
commit 72f45bf6a3
3 changed files with 16 additions and 892 deletions
+3 -397
View File
@@ -49,8 +49,6 @@
}
let loading = $state(true)
let showSpinner = $state(false)
let loadingTimer: ReturnType<typeof setTimeout> | null = null
let error = $state<string | null>(null)
let submitting = $state(false)
let consentData = $state<ConsentData | null>(null)
@@ -140,11 +138,6 @@
error = $_('oauth.error.genericError')
} finally {
loading = false
showSpinner = false
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
}
}
@@ -252,17 +245,7 @@
}
$effect(() => {
loadingTimer = setTimeout(() => {
if (loading) {
showSpinner = true
}
}, 5000)
fetchConsentData()
return () => {
if (loadingTimer) {
clearTimeout(loadingTimer)
}
}
})
let scopeGroups = $derived(consentData ? groupScopesByCategory(consentData.scopes) : [])
@@ -297,16 +280,7 @@
<div class="consent-container">
{#if loading}
<div class="loading">
{#if showSpinner}
<div class="loading-content">
<div class="spinner"></div>
<p>{$_('common.loading')}</p>
</div>
{:else}
<p style="color: var(--text-muted); font-size: 0.875rem;">Loading consent data...</p>
{/if}
</div>
<div class="loading"></div>
{:else if error}
<div class="error-container">
<h1>{$_('oauth.error.title')}</h1>
@@ -421,10 +395,10 @@
</div>
<div class="actions">
<button type="button" class="deny-btn" onclick={handleDeny} disabled={submitting}>
<button type="button" class="cancel" onclick={handleDeny} disabled={submitting}>
{$_('oauth.consent.deny')}
</button>
<button type="button" class="approve-btn" onclick={submitConsent} disabled={submitting}>
<button type="button" onclick={submitConsent} disabled={submitting}>
{submitting ? $_('oauth.consent.authorizing') : $_('oauth.consent.authorize')}
</button>
</div>
@@ -443,371 +417,3 @@
</div>
{/if}
</div>
<style>
.consent-container {
max-width: var(--width-lg);
margin: var(--space-7) auto;
padding: var(--space-7);
}
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
color: var(--text-secondary);
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.loading-content p {
margin: 0;
color: var(--text-secondary);
}
.error-container {
text-align: center;
max-width: var(--width-sm);
margin: 0 auto;
}
.error {
padding: var(--space-3);
background: var(--error-bg);
border: 1px solid var(--error-border);
border-radius: var(--radius-md);
color: var(--error-text);
margin-bottom: var(--space-4);
}
.client-panel {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.permissions-panel {
min-width: 0;
}
.client-info {
text-align: center;
padding: var(--space-6);
background: var(--bg-secondary);
border-radius: var(--radius-xl);
}
@media (min-width: 800px) {
.client-info {
text-align: left;
}
}
.client-logo {
width: 64px;
height: 64px;
border-radius: var(--radius-xl);
margin-bottom: var(--space-4);
}
.client-info h1 {
margin: 0 0 var(--space-1) 0;
font-size: var(--text-xl);
}
.subtitle {
color: var(--text-secondary);
margin: 0;
}
.client-link {
display: inline-block;
margin-top: var(--space-2);
font-size: var(--text-sm);
color: var(--accent);
text-decoration: none;
}
.client-link:hover {
text-decoration: underline;
}
.account-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-4);
background: var(--bg-secondary);
border-radius: var(--radius-xl);
margin-bottom: var(--space-6);
}
.account-info .label {
font-size: var(--text-xs);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.account-info .did {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-secondary);
word-break: break-all;
}
.account-info .handle {
font-size: var(--text-base);
font-weight: var(--font-medium);
color: var(--text-primary);
}
.delegation-badge {
display: inline-block;
padding: var(--space-1) var(--space-2);
background: var(--accent);
color: var(--text-inverse);
border-radius: var(--radius-md);
font-size: var(--text-xs);
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-3);
}
.delegation-info {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.delegation-info .info-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.delegation-info .handle {
font-weight: var(--font-medium);
color: var(--text-primary);
}
.level-badge {
display: inline-block;
padding: 2px var(--space-2);
background: var(--bg-tertiary);
color: var(--text-primary);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
.level-badge.level-owner {
background: var(--success-bg);
color: var(--success-text);
}
.level-badge.level-admin {
background: var(--accent);
color: var(--text-inverse);
}
.level-badge.level-editor {
background: var(--warning-bg);
color: var(--warning-text);
}
.level-badge.level-viewer {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.permissions-notice {
margin-top: var(--space-3);
padding: var(--space-3);
background: var(--warning-bg);
border: 1px solid var(--warning-border);
border-radius: var(--radius-md);
}
.notice-header {
display: flex;
align-items: center;
gap: var(--space-2);
font-weight: var(--font-semibold);
color: var(--warning-text);
margin-bottom: var(--space-2);
}
.notice-header svg {
flex-shrink: 0;
}
.notice-text {
margin: 0;
font-size: var(--text-sm);
color: var(--warning-text);
line-height: 1.5;
}
.scopes-section {
margin-bottom: var(--space-6);
}
.scopes-section h2 {
font-size: var(--text-base);
margin: 0 0 var(--space-4) 0;
color: var(--text-secondary);
}
.scope-group {
margin-bottom: var(--space-4);
}
.category-title {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
padding-bottom: var(--space-1);
border-bottom: 1px solid var(--border-color);
}
.scope-item {
display: flex;
gap: var(--space-3);
padding: var(--space-3);
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
margin-bottom: var(--space-2);
cursor: pointer;
transition: border-color var(--transition-fast);
overflow: hidden;
}
.scope-item:hover:not(.required) {
border-color: var(--accent);
}
.scope-item.required {
background: var(--bg-secondary);
}
.scope-item.read-only {
background: var(--bg-secondary);
border-style: dashed;
}
.scope-item input[type="checkbox"] {
flex-shrink: 0;
width: 18px;
height: 18px;
margin-top: 2px;
}
.scope-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.scope-name {
font-weight: var(--font-medium);
color: var(--text-primary);
word-break: break-all;
}
.scope-description {
font-size: var(--text-sm);
color: var(--text-secondary);
word-break: break-all;
}
.required-badge {
display: inline-block;
font-size: 0.625rem;
padding: 2px var(--space-2);
background: var(--warning-bg);
color: var(--warning-text);
border-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: var(--space-1);
width: fit-content;
}
.remember-choice {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-5);
cursor: pointer;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.remember-choice input {
width: 16px;
height: 16px;
}
.actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-6);
}
@media (min-width: 800px) {
.actions {
max-width: 400px;
margin-left: auto;
}
}
.actions button {
flex: 1;
padding: var(--space-3);
border: none;
border-radius: var(--radius-lg);
font-size: var(--text-base);
font-weight: var(--font-medium);
cursor: pointer;
transition: background-color var(--transition-fast);
}
.actions button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.deny-btn {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.deny-btn:hover:not(:disabled) {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
.approve-btn {
background: var(--accent);
color: var(--text-inverse);
}
.approve-btn:hover:not(:disabled) {
background: var(--accent-hover);
}
</style>
+8 -291
View File
@@ -401,7 +401,7 @@
{/if}
<form onsubmit={handleSubmit}>
<div class="field">
<div>
<label for="username">{$_('register.handle')}</label>
<input
id="username"
@@ -425,7 +425,7 @@
disabled={submitting || ssoLoading !== null}
>
{#if ssoLoading === provider.provider}
<span class="spinner sm"></span>
<span>{$_('common.loading')}</span>
{:else}
<SsoIcon provider={provider.icon} size={20} />
{/if}
@@ -445,7 +445,7 @@
<h3>{$_('oauth.login.signInWithPasskey')}</h3>
<button
type="button"
class="passkey-btn"
style="width: 100%"
class:passkey-unavailable={!hasPasskeys || checkingSecurityStatus || !securityStatusChecked}
onclick={handlePasskeyLogin}
disabled={submitting || !hasPasskeys || !username || checkingSecurityStatus || !securityStatusChecked}
@@ -494,7 +494,7 @@
<span>{$_('oauth.login.rememberDevice')}</span>
</label>
<button type="submit" class="submit-btn" disabled={submitting || !username || !password}>
<button type="submit" disabled={submitting || !username || !password}>
{submitting ? $_('oauth.login.signingIn') : $_('oauth.login.title')}
</button>
</div>
@@ -502,13 +502,13 @@
</div>
<div class="cancel-row">
<button type="button" class="cancel-btn-subtle" onclick={handleCancel} disabled={submitting}>
<button type="button" class="ghost sm" onclick={handleCancel} disabled={submitting}>
{$_('common.cancel')}
</button>
</div>
{:else}
{#if hasPassword || !securityStatusChecked}
<div class="field">
<div>
<label for="password">{$_('oauth.login.password')}</label>
<input
id="password"
@@ -526,14 +526,14 @@
</label>
<div class="actions">
<button type="submit" class="submit-btn" disabled={submitting || !username || !password}>
<button type="submit" disabled={submitting || !username || !password}>
{submitting ? $_('oauth.login.signingIn') : $_('oauth.login.title')}
</button>
</div>
{/if}
<div class="cancel-row">
<button type="button" class="cancel-btn-subtle" onclick={handleCancel} disabled={submitting}>
<button type="button" class="ghost sm" onclick={handleCancel} disabled={submitting}>
{$_('common.cancel')}
</button>
</div>
@@ -544,286 +544,3 @@
<a href={getFullUrl(routes.resetPassword)}>{$_('login.forgotPassword')}</a> &middot; <a href={getFullUrl(routes.requestPasskeyRecovery)}>{$_('login.lostPasskey')}</a>
</p>
</div>
<style>
.help-links {
text-align: center;
margin-top: var(--space-4);
font-size: var(--text-sm);
}
.help-links a {
color: var(--accent);
text-decoration: none;
}
.help-links a:hover {
text-decoration: underline;
}
form {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.auth-methods {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-5);
margin-top: var(--space-4);
}
@media (min-width: 600px) {
.auth-methods {
grid-template-columns: 1fr auto 1fr;
align-items: start;
}
}
.auth-methods.single-method {
grid-template-columns: 1fr;
}
@media (min-width: 600px) {
.auth-methods.single-method {
grid-template-columns: 1fr;
max-width: 400px;
margin: var(--space-4) auto 0;
}
}
.passkey-method,
.password-method {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-5);
background: var(--bg-secondary);
border-radius: var(--radius-xl);
}
.passkey-method h3,
.password-method h3 {
margin: 0;
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.method-divider {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: var(--text-sm);
}
@media (min-width: 600px) {
.method-divider {
flex-direction: column;
padding: 0 var(--space-3);
}
.method-divider::before,
.method-divider::after {
content: '';
width: 1px;
height: var(--space-6);
background: var(--border-color);
}
.method-divider span {
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
padding: var(--space-2) 0;
}
}
@media (max-width: 599px) {
.method-divider {
gap: var(--space-4);
}
.method-divider::before,
.method-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border-color);
}
}
.remember-device {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.remember-device input {
width: 16px;
height: 16px;
}
.actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-2);
}
.actions button {
flex: 1;
}
.cancel-row {
display: flex;
justify-content: center;
margin-top: var(--space-4);
}
.cancel-btn-subtle {
padding: var(--space-2) var(--space-4);
background: transparent;
color: var(--text-muted);
border: none;
border-radius: var(--radius-md);
font-size: var(--text-sm);
cursor: pointer;
transition: color var(--transition-fast);
}
.cancel-btn-subtle:hover:not(:disabled) {
color: var(--text-secondary);
}
.cancel-btn-subtle:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.submit-btn {
background: var(--accent);
color: var(--text-inverse);
}
.submit-btn:hover:not(:disabled) {
background: var(--accent-hover);
}
.passkey-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-3);
background: var(--accent);
color: var(--text-inverse);
border: 1px solid var(--accent);
border-radius: var(--radius-md);
font-size: var(--text-base);
cursor: pointer;
transition: background-color var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast);
}
.passkey-btn:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.passkey-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.passkey-btn.passkey-unavailable {
background: var(--bg-secondary);
color: var(--text-secondary);
border-color: var(--border-color);
}
.passkey-icon {
width: 20px;
height: 20px;
}
.passkey-text {
flex: 1;
text-align: left;
}
.sso-section {
margin-top: var(--space-6);
}
.sso-section-top {
margin-top: var(--space-4);
margin-bottom: 0;
}
.sso-section-top .sso-divider {
margin-top: var(--space-5);
margin-bottom: 0;
}
.sso-divider {
display: flex;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-4);
color: var(--text-muted);
font-size: var(--text-sm);
}
.sso-divider::before,
.sso-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border-color);
}
.sso-buttons {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
justify-content: center;
}
.sso-btn {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: var(--text-sm);
cursor: pointer;
transition: background-color var(--transition-fast), border-color var(--transition-fast);
}
.sso-btn-prominent {
padding: var(--space-3) var(--space-5);
font-size: var(--text-base);
font-weight: var(--font-medium);
}
.sso-btn:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--accent);
}
.sso-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
+5 -204
View File
@@ -308,10 +308,7 @@
<div class="oauth-register-container">
{#if loadingServerInfo}
<div class="loading">
<div class="spinner"></div>
<p>{$_('common.loading')}</p>
</div>
<div class="loading"></div>
{:else if flow}
<header class="page-header">
<h1>{$_('oauth.register.title')}</h1>
@@ -345,7 +342,7 @@
<div class="split-layout">
<div class="form-section">
<form onsubmit={handleInfoSubmit}>
<div class="field">
<div>
<label for="handle">{$_('register.handle')}</label>
<HandleInput
value={flow.info.handle}
@@ -484,7 +481,7 @@
</fieldset>
{#if serverInfo?.inviteCodeRequired}
<div class="field">
<div>
<label for="invite-code">{$_('register.inviteCode')} <span class="required">*</span></label>
<input
id="invite-code"
@@ -504,10 +501,10 @@
</div>
<div class="secondary-actions">
<button type="button" class="link-btn" onclick={goToLogin}>
<button type="button" class="link" onclick={goToLogin}>
{$_('oauth.register.haveAccount')}
</button>
<button type="button" class="link-btn" onclick={handleCancel}>
<button type="button" class="link" onclick={handleCancel}>
{$_('common.cancel')}
</button>
</div>
@@ -540,7 +537,6 @@
{:else if flow.state.step === 'creating'}
<div class="creating">
<div class="spinner"></div>
<p>{$_('registerPasskey.creatingAccount')}</p>
</div>
@@ -582,203 +578,8 @@
{:else if flow.state.step === 'activating'}
<div class="creating">
<div class="spinner"></div>
<p>{$_('registerPasskey.activatingAccount')}</p>
</div>
{/if}
{/if}
</div>
<style>
.oauth-register-container {
max-width: var(--width-lg);
margin: var(--space-9) auto;
padding: var(--space-7);
}
.loading, .creating {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
padding: var(--space-8);
}
.loading p, .creating p {
color: var(--text-secondary);
}
.page-header {
margin-bottom: var(--space-6);
}
.page-header h1 {
margin: 0 0 var(--space-2) 0;
}
.subtitle {
color: var(--text-secondary);
margin: 0;
}
.form-section {
min-width: 0;
}
.form-links {
margin-top: var(--space-6);
}
.link-text {
text-align: center;
color: var(--text-secondary);
}
.link-text a {
color: var(--accent);
}
form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
}
input, select {
padding: var(--space-3);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: var(--text-base);
background: var(--bg-input);
color: var(--text-primary);
}
input:focus, select:focus {
outline: none;
border-color: var(--accent);
}
.hint {
font-size: var(--text-xs);
color: var(--text-muted);
margin: var(--space-1) 0 0 0;
}
.error {
padding: var(--space-3);
background: var(--error-bg);
border: 1px solid var(--error-border);
border-radius: var(--radius-md);
color: var(--error-text);
margin-bottom: var(--space-4);
}
.actions {
display: flex;
gap: var(--space-4);
margin-top: var(--space-2);
}
button.primary {
flex: 1;
padding: var(--space-3);
background: var(--accent);
color: var(--text-inverse);
border: none;
border-radius: var(--radius-md);
font-size: var(--text-base);
cursor: pointer;
transition: background-color var(--transition-fast);
}
button.primary:hover:not(:disabled) {
background: var(--accent-hover);
}
button.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.secondary-actions {
display: flex;
justify-content: center;
gap: var(--space-4);
margin-top: var(--space-4);
}
.link-btn {
background: none;
border: none;
color: var(--accent);
cursor: pointer;
font-size: var(--text-sm);
padding: var(--space-2);
}
.link-btn:hover {
text-decoration: underline;
}
.contact-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.required {
color: var(--error-text);
}
.passkey-step {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.passkey-step h2 {
margin: 0;
}
.passkey-step p {
color: var(--text-secondary);
margin: 0;
}
fieldset {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-4);
}
legend {
padding: 0 var(--space-2);
font-weight: var(--font-medium);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>