style: apply prettier formatting to manascore docs, todo web, and auth-ui pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-01 15:28:30 +02:00
parent b684ddeeda
commit 1007c1e82b
10 changed files with 1349 additions and 1163 deletions

View file

@ -179,156 +179,160 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 shadow-lg"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<h2
class="text-xl font-semibold text-center mb-6"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 shadow-lg"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
{mode === 'form' ? t.titleForm : t.titleSuccess}
</h2>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Error Messages -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<h2
class="text-xl font-semibold text-center mb-6"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
<p>{error}</p>
</div>
{/if}
{mode === 'form' ? t.titleForm : t.titleSuccess}
</h2>
<!-- Form Mode -->
{#if mode === 'form'}
<form
onsubmit={(e) => {
e.preventDefault();
handleForgotPassword();
}}
>
<p
class="mb-4 text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
<!-- Error Messages -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
{t.description}
</p>
<div class="mb-4">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
<p>{error}</p>
</div>
{/if}
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
<!-- Form Mode -->
{#if mode === 'form'}
<form
onsubmit={(e) => {
e.preventDefault();
handleForgotPassword();
}}
>
<Key size={20} />
{loading ? t.sending : t.sendResetLinkButton}
</button>
</form>
<!-- Back Button -->
<div class="mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center flex items-center justify-center gap-2 transition-opacity hover:opacity-70"
style:color={isDark ? '#fff' : '#000'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
<!-- Success Mode -->
{:else}
<div class="pb-4">
<div class="flex flex-col items-center mb-6">
<div
class="w-20 h-20 rounded-full flex items-center justify-center mb-6"
style:background-color={primaryColor + '30'}
style:color={primaryColor}
>
<EnvelopeOpen size={40} />
</div>
<p
class="text-sm text-center px-2"
class="mb-4 text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{getSuccessMessage(resetEmail)}
{t.description}
</p>
</div>
<div class="flex flex-col gap-3">
<div class="mb-4">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<button
type="button"
onclick={() => goto(loginPath)}
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
<SignIn size={20} />
{t.backToLogin}
<Key size={20} />
{loading ? t.sending : t.sendResetLinkButton}
</button>
</form>
<!-- Back Button -->
<div class="mt-4">
<button
type="button"
onclick={() => {
mode = 'form';
error = null;
}}
class="w-full h-10 border rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-70"
style:background-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
onclick={() => goto(loginPath)}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center flex items-center justify-center gap-2 transition-opacity hover:opacity-70"
style:color={isDark ? '#fff' : '#000'}
>
{t.resendEmail}
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
</div>
{/if}
<!-- Success Mode -->
{:else}
<div class="pb-4">
<div class="flex flex-col items-center mb-6">
<div
class="w-20 h-20 rounded-full flex items-center justify-center mb-6"
style:background-color={primaryColor + '30'}
style:color={primaryColor}
>
<EnvelopeOpen size={40} />
</div>
<p
class="text-sm text-center px-2"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{getSuccessMessage(resetEmail)}
</p>
</div>
<div class="flex flex-col gap-3">
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
<SignIn size={20} />
{t.backToLogin}
</button>
<button
type="button"
onclick={() => {
mode = 'form';
error = null;
}}
class="w-full h-10 border rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-70"
style:background-color={isDark
? 'rgba(255,255,255,0.1)'
: 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
>
{t.resendEmail}
</button>
</div>
</div>
{/if}
</div>
</div>
</div>
</main>
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}

View file

@ -460,341 +460,315 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<button
type="button"
onclick={fillDevCredentials}
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105 active:scale-95"
class:success-pulse={showSuccess}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
aria-label="{appName} logo"
>
{#if showSuccess}
<Check size={55} class="text-green-500" />
{:else}
<Logo size={55} color={primaryColor} />
{/if}
</button>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
class:shake={shakeError}
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if showTwoFactor}
<!-- 2FA Verification -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.twoFactorTitle}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{useBackupCode ? t.twoFactorBackupSubtitle : t.twoFactorSubtitle}
</p>
</div>
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<Warning size={18} class="text-red-500 shrink-0" />
<p>{error}</p>
</div>
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<button
type="button"
onclick={fillDevCredentials}
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105 active:scale-95"
class:success-pulse={showSuccess}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
aria-label="{appName} logo"
>
{#if showSuccess}
<Check size={55} class="text-green-500" />
{:else}
<Logo size={55} color={primaryColor} />
{/if}
</button>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<form
onsubmit={(e) => {
e.preventDefault();
handleTwoFactorVerify();
}}
>
<div class="mb-3">
<input
type="text"
bind:value={twoFactorCode}
placeholder={useBackupCode ? t.twoFactorBackupPlaceholder : '000000'}
required
autocomplete="one-time-code"
inputmode={useBackupCode ? 'text' : 'numeric'}
maxlength={useBackupCode ? 20 : 6}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:text-align="center"
style:font-size="1.5rem"
style:letter-spacing="0.5rem"
style:--tw-ring-color="var(--ring-color)"
/>
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
class:shake={shakeError}
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if showTwoFactor}
<!-- 2FA Verification -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.twoFactorTitle}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{useBackupCode ? t.twoFactorBackupSubtitle : t.twoFactorSubtitle}
</p>
</div>
{#if !useBackupCode}
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:margin-bottom="1rem"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<input
type="checkbox"
bind:checked={trustDevice}
style:accent-color={primaryColor}
/>
<span>{t.twoFactorTrustDevice}</span>
</label>
<Warning size={18} class="text-red-500 shrink-0" />
<p>{error}</p>
</div>
{/if}
<button
type="submit"
disabled={loading || !twoFactorCode}
aria-disabled={loading || !twoFactorCode}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
<form
onsubmit={(e) => {
e.preventDefault();
handleTwoFactorVerify();
}}
>
{loading ? t.twoFactorVerifying : t.twoFactorConfirm}
</button>
</form>
<div class="mb-3">
<input
type="text"
bind:value={twoFactorCode}
placeholder={useBackupCode ? t.twoFactorBackupPlaceholder : '000000'}
required
autocomplete="one-time-code"
inputmode={useBackupCode ? 'text' : 'numeric'}
maxlength={useBackupCode ? 20 : 6}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:--ring-color={primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:text-align="center"
style:font-size="1.5rem"
style:letter-spacing="0.5rem"
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-4"
style:color={primaryColor}
onclick={() => {
useBackupCode = !useBackupCode;
twoFactorCode = '';
clearError();
}}
>
{useBackupCode ? t.twoFactorUseAuthenticator : t.twoFactorUseBackupCode}
</button>
{#if !useBackupCode}
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:margin-bottom="1rem"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={trustDevice}
style:accent-color={primaryColor}
/>
<span>{t.twoFactorTrustDevice}</span>
</label>
{/if}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-2"
style:color={primaryColor}
onclick={() => {
showTwoFactor = false;
twoFactorCode = '';
useBackupCode = false;
clearError();
}}
>
{t.twoFactorBackToLogin}
</button>
{:else}
{#if showVerifiedBanner}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.emailVerified}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (showVerifiedBanner = false)}
aria-label="Close"
type="submit"
disabled={loading || !twoFactorCode}
aria-disabled={loading || !twoFactorCode}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={primaryColor + '60'}
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
&times;
{loading ? t.twoFactorVerifying : t.twoFactorConfirm}
</button>
</div>
{/if}
</form>
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.subtitle}
</p>
</div>
{#if passkeyAvailable && onSignInWithPasskey}
<button
type="button"
onclick={handlePasskeySignIn}
disabled={loading || showSuccess}
aria-disabled={loading || showSuccess}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity bg-transparent hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:border-color={primaryColor}
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-4"
style:color={primaryColor}
onclick={() => {
useBackupCode = !useBackupCode;
twoFactorCode = '';
clearError();
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
<span>Passkey</span>
{useBackupCode ? t.twoFactorUseAuthenticator : t.twoFactorUseBackupCode}
</button>
<div class="divider flex items-center gap-4 my-5">
<span
class="text-xs"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>{t.orDivider}</span
>
</div>
{/if}
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70 block w-full text-center mt-2"
style:color={primaryColor}
onclick={() => {
showTwoFactor = false;
twoFactorCode = '';
useBackupCode = false;
clearError();
}}
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.verificationEmailSent}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (verificationEmailSent = false)}
aria-label="Close"
{t.twoFactorBackToLogin}
</button>
{:else}
{#if showVerifiedBanner}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
&times;
</button>
</div>
{/if}
{#if isLockedOut}
<div
class="flex gap-3 p-4 mb-4 rounded-xl bg-amber-500/15 border border-amber-500/30 text-amber-500"
role="alert"
aria-live="assertive"
>
<div class="shrink-0 mt-0.5">
<Warning size={24} />
</div>
<div class="flex flex-col gap-1">
<p class="font-semibold text-[0.9rem]">{t.accountLocked}</p>
<p class="text-[0.8rem] opacity-90">
{t.tooManyAttempts}
{#if rateLimitCountdown > 0}
{t.retryIn} <strong>{formatCountdown(rateLimitCountdown)}</strong>
{/if}
</p>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.emailVerified}</p>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-[0.8rem] p-0 text-left underline mt-1"
onclick={() => goto(forgotPasswordPath)}
style:color={primaryColor}
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (showVerifiedBanner = false)}
aria-label="Close"
>
{t.resetPassword}
&times;
</button>
</div>
{/if}
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<p
class="text-sm mt-2"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.subtitle}
</p>
</div>
{:else if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
id="form-error"
role="alert"
aria-live="assertive"
>
<Warning size={18} class="text-red-500 shrink-0" />
<div class="flex flex-col gap-1">
<p>{error}</p>
{#if showEmailNotVerified && onResendVerification}
{#if passkeyAvailable && onSignInWithPasskey}
<button
type="button"
onclick={handlePasskeySignIn}
disabled={loading || showSuccess}
aria-disabled={loading || showSuccess}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity bg-transparent hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:border-color={primaryColor}
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
<span>Passkey</span>
</button>
<div class="divider flex items-center gap-4 my-5">
<span
class="text-xs"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>{t.orDivider}</span
>
</div>
{/if}
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.verificationEmailSent}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (verificationEmailSent = false)}
aria-label="Close"
>
&times;
</button>
</div>
{/if}
{#if isLockedOut}
<div
class="flex gap-3 p-4 mb-4 rounded-xl bg-amber-500/15 border border-amber-500/30 text-amber-500"
role="alert"
aria-live="assertive"
>
<div class="shrink-0 mt-0.5">
<Warning size={24} />
</div>
<div class="flex flex-col gap-1">
<p class="font-semibold text-[0.9rem]">{t.accountLocked}</p>
<p class="text-[0.8rem] opacity-90">
{t.tooManyAttempts}
{#if rateLimitCountdown > 0}
{t.retryIn} <strong>{formatCountdown(rateLimitCountdown)}</strong>
{/if}
</p>
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-sm p-0 text-left underline hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="bg-transparent border-none cursor-pointer font-medium text-[0.8rem] p-0 text-left underline mt-1"
onclick={() => goto(forgotPasswordPath)}
style:color={primaryColor}
>
{resendingVerification ? t.resendingVerification : t.resendVerification}
{t.resetPassword}
</button>
{/if}
{#if rateLimitCountdown > 0}
<p class="font-semibold mt-1">
{t.retryIn}
{formatCountdown(rateLimitCountdown)}
</p>
{/if}
</div>
</div>
</div>
{/if}
{:else if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
id="form-error"
role="alert"
aria-live="assertive"
>
<Warning size={18} class="text-red-500 shrink-0" />
<div class="flex flex-col gap-1">
<p>{error}</p>
{#if showEmailNotVerified && onResendVerification}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium text-sm p-0 text-left underline hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
style:color={primaryColor}
>
{resendingVerification ? t.resendingVerification : t.resendVerification}
</button>
{/if}
{#if rateLimitCountdown > 0}
<p class="font-semibold mt-1">
{t.retryIn}
{formatCountdown(rateLimitCountdown)}
</p>
{/if}
</div>
</div>
{/if}
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
aria-busy={loading}
>
<!-- Email -->
<div class="mb-3">
<label for="email" class="sr-only">{t.emailPlaceholder}</label>
<input
id="email"
type="email"
bind:this={emailInput}
bind:value={email}
placeholder={t.emailPlaceholder}
required
autocomplete={passkeyAvailable ? 'username webauthn' : 'email'}
aria-invalid={errorField === 'email'}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'email'}
style:--ring-color={errorField === 'email' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'email'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
: 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<!-- Password -->
<div class="mb-3">
<label for="password" class="sr-only">{t.passwordPlaceholder}</label>
<div class="relative">
<form
onsubmit={(e) => {
e.preventDefault();
handleLogin();
}}
aria-busy={loading}
>
<!-- Email -->
<div class="mb-3">
<label for="email" class="sr-only">{t.emailPlaceholder}</label>
<input
id="password"
type={showPassword ? 'text' : 'password'}
bind:this={passwordInput}
bind:value={password}
placeholder={t.passwordPlaceholder}
id="email"
type="email"
bind:this={emailInput}
bind:value={email}
placeholder={t.emailPlaceholder}
required
autocomplete="current-password"
aria-invalid={errorField === 'password'}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'password'}
style:--ring-color={errorField === 'password' ? '#ef4444' : primaryColor}
autocomplete={passkeyAvailable ? 'username webauthn' : 'email'}
aria-invalid={errorField === 'email'}
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'email'}
style:--ring-color={errorField === 'email' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'password'
style:border-color={errorField === 'email'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
@ -802,150 +776,178 @@
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
</div>
<!-- Password -->
<div class="mb-3">
<label for="password" class="sr-only">{t.passwordPlaceholder}</label>
<div class="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
bind:this={passwordInput}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
autocomplete="current-password"
aria-invalid={errorField === 'password'}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
class:border-red-500={errorField === 'password'}
style:--ring-color={errorField === 'password' ? '#ef4444' : primaryColor}
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={errorField === 'password'
? '#ef4444'
: isDark
? 'rgba(255,255,255,0.2)'
: 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color="var(--ring-color)"
/>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity hover:opacity-80"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Remember & Forgot -->
<div class="flex justify-between items-center mb-4 text-sm">
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={rememberMe}
style:accent-color={primaryColor}
/>
<span>{t.rememberMe}</span>
</label>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity hover:opacity-80"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
onclick={() => goto(forgotPasswordPath)}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
style:color={primaryColor}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
{t.forgotPassword}
</button>
</div>
</div>
<!-- Remember & Forgot -->
<div class="flex justify-between items-center mb-4 text-sm">
<label
class="remember-label flex items-center gap-2 cursor-pointer"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<input
type="checkbox"
bind:checked={rememberMe}
style:accent-color={primaryColor}
/>
<span>{t.rememberMe}</span>
</label>
<!-- Submit -->
<button
type="button"
onclick={() => goto(forgotPasswordPath)}
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
style:color={primaryColor}
type="submit"
disabled={loading || showSuccess || rateLimitCountdown > 0}
aria-disabled={loading || showSuccess || rateLimitCountdown > 0}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{t.forgotPassword}
{#if loading}
<svg
class="spinner"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.signingIn}</span>
{:else if showSuccess}
<Check size={20} />
<span>{t.success}</span>
{:else}
<SignIn size={20} />
<span>{t.signInButton}</span>
{/if}
</button>
</div>
</form>
<!-- Submit -->
<button
type="submit"
disabled={loading || showSuccess || rateLimitCountdown > 0}
aria-disabled={loading || showSuccess || rateLimitCountdown > 0}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if loading}
<svg
class="spinner"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
{#if onSendMagicLink}
{#if magicLinkSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.signingIn}</span>
{:else if showSuccess}
<Check size={20} />
<span>{t.success}</span>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.magicLinkSent?.replace('{email}', email)}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (magicLinkSent = false)}
aria-label="Close"
>
&times;
</button>
</div>
{:else}
<SignIn size={20} />
<span>{t.signInButton}</span>
{/if}
</button>
</form>
{#if onSendMagicLink}
{#if magicLinkSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl relative text-sm bg-green-500/15 border border-green-500/30 text-green-500"
role="status"
aria-live="polite"
>
<Check size={18} class="text-green-500 shrink-0" />
<p>{t.magicLinkSent?.replace('{email}', email)}</p>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none text-green-500 text-xl cursor-pointer p-1 leading-none opacity-70 hover:opacity-100"
onclick={() => (magicLinkSent = false)}
aria-label="Close"
onclick={handleSendMagicLink}
disabled={sendingMagicLink || !email}
aria-disabled={sendingMagicLink || !email}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center transition-opacity hover:opacity-70 disabled:opacity-40 disabled:cursor-not-allowed"
style:color={primaryColor}
>
&times;
{sendingMagicLink ? t.magicLinkSending : t.magicLinkButton}
</button>
</div>
{:else}
{/if}
{/if}
<p
class="text-center text-sm mt-4"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.noAccount}
<button
type="button"
onclick={handleSendMagicLink}
disabled={sendingMagicLink || !email}
aria-disabled={sendingMagicLink || !email}
class="w-full bg-transparent border-none cursor-pointer font-medium text-sm p-3 text-center transition-opacity hover:opacity-70 disabled:opacity-40 disabled:cursor-not-allowed"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
onclick={() => goto(registerPath)}
style:color={primaryColor}
>
{sendingMagicLink ? t.magicLinkSending : t.magicLinkButton}
{t.createAccount}
</button>
{/if}
</p>
{/if}
<p
class="text-center text-sm mt-4"
style:color={isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.6)'}
>
{t.noAccount}
<button
type="button"
class="bg-transparent border-none cursor-pointer font-medium p-1 hover:opacity-70"
onclick={() => goto(registerPath)}
style:color={primaryColor}
>
{t.createAccount}
</button>
</p>
{/if}
</div>
</div>
{#if version}
<p
class="text-[10px] text-gray-400/60 select-none pointer-events-none m-0 pt-2 pb-1 text-center"
>
v{version}{#if buildTime}
· {new Date(buildTime).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
{new Date(buildTime).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}{/if}
</p>
{/if}
</div>
</main>
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}
{#if version}
<p
class="fixed bottom-2 right-3 text-[10px] text-gray-400/60 select-none pointer-events-none m-0"
>
v{version}{#if buildTime}
· {new Date(buildTime).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
{new Date(buildTime).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
})}{/if}
</p>
{/if}
</div>
<style>

View file

@ -286,321 +286,323 @@
</div>
{/if}
<main class="flex-1 flex flex-col">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-12 max-[480px]:pt-8 px-4 pb-6 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Form Section -->
<div class="flex-1 flex justify-center px-4 pt-4 pb-8">
<div
class="w-full max-w-[400px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{t.title}
</h2>
<main class="flex-1 flex flex-col items-center justify-center">
<div class="w-full max-w-[480px] mx-auto px-4 flex flex-col items-center">
<!-- Logo Section -->
<div class="flex flex-col items-center pt-8 max-[480px]:pt-6 pb-4 anim-fade-in-scale">
<div
class="w-[100px] h-[100px] max-[480px]:w-[80px] max-[480px]:h-[80px] rounded-full border-[3px] flex items-center justify-center mb-3 cursor-pointer transition-transform shadow-lg hover:scale-105"
style:border-color={primaryColor}
style:background-color={isDark ? '#000' : '#fff'}
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style:color={isDark ? '#fff' : '#000'}>{appName}</h1>
</div>
<!-- Error Message -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<span>&#9888;</span>
<p>{error}</p>
</div>
{/if}
<!-- Verification Email Sent -->
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl text-sm bg-green-500/15 border border-green-500/30 text-green-500"
>
<span>{t.verificationEmailSent}</span>
</div>
{/if}
<!-- Success: Needs Verification / Already Registered -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5 border-2"
class:bg-green-500={false}
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 15%, transparent)'
: 'color-mix(in srgb, #22c55e 15%, transparent)'}
style:border-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 40%, transparent)'
: 'color-mix(in srgb, #22c55e 40%, transparent)'}
>
<div class="flex items-start gap-3 mb-4">
<div
class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 20%, transparent)'
: 'color-mix(in srgb, #22c55e 20%, transparent)'}
>
{#if emailAlreadyRegistered}
<svg
class="w-5 h-5"
style:color="#f59e0b"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{:else}
<svg
class="w-5 h-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
{/if}
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style:color={emailAlreadyRegistered
? isDark
? '#fbbf24'
: '#d97706'
: isDark
? '#22c55e'
: '#16a34a'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegistered || 'Email already registered'
: t.checkYourEmail || 'Check your email'}
</h3>
<p
class="text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegisteredMessage ||
"An account with this email already exists. If you haven't verified your email yet, resend the verification email."
: t.accountCreated}
</p>
</div>
</div>
<div
class="pt-3 border-t flex flex-col gap-2"
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
<!-- Form Section -->
<div class="w-full flex justify-center pt-2 pb-8">
<div
class="w-full max-w-[440px] rounded-2xl p-6 max-[480px]:p-5 border backdrop-blur-[10px] anim-fade-in-up"
style:background-color={isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'}
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<!-- Title -->
<div class="text-center mb-6">
<h2
class="text-xl font-semibold"
style:color={isDark ? 'rgba(255,255,255,0.9)' : 'rgba(0,0,0,0.9)'}
>
{#if onResendVerification}
<button
type="button"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed border-[1.5px]"
style:background-color="{primaryColor}40"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
{t.title}
</h2>
</div>
<!-- Error Message -->
{#if error}
<div
class="flex items-start gap-2 p-3 mb-4 rounded-xl text-sm bg-red-500/15 border border-red-500/30 text-red-500"
role="alert"
>
<span>&#9888;</span>
<p>{error}</p>
</div>
{/if}
<!-- Verification Email Sent -->
{#if verificationEmailSent}
<div
class="flex items-center gap-2 p-3 mb-4 rounded-xl text-sm bg-green-500/15 border border-green-500/30 text-green-500"
>
<span>{t.verificationEmailSent}</span>
</div>
{/if}
<!-- Success: Needs Verification / Already Registered -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5 border-2"
class:bg-green-500={false}
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 15%, transparent)'
: 'color-mix(in srgb, #22c55e 15%, transparent)'}
style:border-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 40%, transparent)'
: 'color-mix(in srgb, #22c55e 40%, transparent)'}
>
<div class="flex items-start gap-3 mb-4">
<div
class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center"
style:background-color={emailAlreadyRegistered
? 'color-mix(in srgb, #f59e0b 20%, transparent)'
: 'color-mix(in srgb, #22c55e 20%, transparent)'}
>
{#if resendingVerification}
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
fill="none"
></circle>
{#if emailAlreadyRegistered}
<svg
class="w-5 h-5"
style:color="#f59e0b"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{t.resendingVerification}
{:else}
{t.resendVerification}
<svg
class="w-5 h-5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
{/if}
</button>
{/if}
{#if emailAlreadyRegistered}
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style:color={emailAlreadyRegistered
? isDark
? '#fbbf24'
: '#d97706'
: isDark
? '#22c55e'
: '#16a34a'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegistered || 'Email already registered'
: t.checkYourEmail || 'Check your email'}
</h3>
<p
class="text-sm"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
{emailAlreadyRegistered
? t.emailAlreadyRegisteredMessage ||
"An account with this email already exists. If you haven't verified your email yet, resend the verification email."
: t.accountCreated}
</p>
</div>
</div>
<div
class="pt-3 border-t flex flex-col gap-2"
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
{#if onResendVerification}
<button
type="button"
onclick={handleResendVerification}
disabled={resendingVerification}
aria-disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-opacity hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed border-[1.5px]"
style:background-color="{primaryColor}40"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if resendingVerification}
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
fill="none"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t.resendingVerification}
{:else}
{t.resendVerification}
{/if}
</button>
{/if}
{#if emailAlreadyRegistered}
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full flex items-center justify-center h-11 rounded-lg font-medium transition-opacity hover:opacity-80 border-[1.5px]"
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
style:background-color="transparent"
>
{t.goToLogin || 'Sign in instead'}
</button>
{/if}
</div>
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
>
<!-- Email -->
<div class="mb-3">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
</div>
<!-- Password -->
<div class="mb-3">
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => goto(loginPath)}
class="w-full flex items-center justify-center h-11 rounded-lg font-medium transition-opacity hover:opacity-80 border-[1.5px]"
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
style:background-color="transparent"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{t.goToLogin || 'Sign in instead'}
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
{/if}
</div>
</div>
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
>
<!-- Email -->
<div class="mb-3">
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="w-full h-14 px-4 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
<PasswordStrength {password} {primaryColor} />
<!-- Confirm Password -->
<div class="mb-3">
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
bind:value={confirmPassword}
placeholder={t.confirmPasswordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Password Requirements -->
<p
class="text-xs mb-3"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>
{t.passwordRequirements}
</p>
<!-- Submit -->
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color="{primaryColor}60"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
>
{#if loading}
<svg
class="w-5 h-5 animate-spin"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.creatingAccount}</span>
{:else}
<UserPlus size={20} />
<span>{t.createAccountButton}</span>
{/if}
</button>
</form>
<!-- Back to Login -->
<div class="text-center mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="inline-flex items-center gap-2 bg-transparent border-none cursor-pointer font-medium transition-opacity hover:opacity-70"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
<!-- Password -->
<div class="mb-3">
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<PasswordStrength {password} {primaryColor} />
<!-- Confirm Password -->
<div class="mb-3">
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
bind:value={confirmPassword}
placeholder={t.confirmPasswordPlaceholder}
required
minlength={8}
class="w-full h-14 px-4 pr-12 border rounded-xl text-base transition-colors focus:outline-none focus:ring-2"
style:background-color={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.8)'}
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}
style:color={isDark ? '#fff' : '#000'}
style:--tw-ring-color={primaryColor}
/>
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute right-0 top-0 h-full w-12 flex items-center justify-center bg-transparent border-none cursor-pointer transition-opacity"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)'}
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<!-- Password Requirements -->
<p
class="text-xs mb-3"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>
{t.passwordRequirements}
</p>
<!-- Submit -->
<button
type="submit"
disabled={loading}
aria-disabled={loading}
class="w-full h-14 border-2 rounded-xl font-medium flex items-center justify-center gap-2 cursor-pointer transition-opacity hover:opacity-85 disabled:opacity-50 disabled:cursor-not-allowed"
style:background-color="{primaryColor}60"
style:border-color={primaryColor}
style:color={isDark ? '#fff' : '#000'}
>
{#if loading}
<svg
class="w-5 h-5 animate-spin"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25" />
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg>
<span>{t.creatingAccount}</span>
{:else}
<UserPlus size={20} />
<span>{t.createAccountButton}</span>
{/if}
</button>
</form>
<!-- Back to Login -->
<div class="text-center mt-4">
<button
type="button"
onclick={() => goto(loginPath)}
class="inline-flex items-center gap-2 bg-transparent border-none cursor-pointer font-medium transition-opacity hover:opacity-70"
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
>
<ArrowLeft size={20} />
{t.backToLogin}
</button>
</div>
</div>
</div>
@ -608,7 +610,7 @@
<!-- App Slider -->
{#if appSlider}
<footer class="w-full pb-4 anim-fade-in">
<footer class="w-full max-w-[640px] mx-auto pb-4 anim-fade-in">
{@render appSlider()}
</footer>
{/if}