refactor(auth-ui): rewrite LoginPage + RegisterPage to use Tailwind CSS

Replace ~700 lines of scoped CSS with Tailwind utility classes for
consistency with the rest of the monorepo. Both pages now use identical
patterns: Tailwind for layout/sizing/spacing, style: bindings for
dynamic dark/light colors, minimal <style> block for keyframe animations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 13:16:14 +02:00
parent b3541957bd
commit 97798e5382
2 changed files with 527 additions and 982 deletions

File diff suppressed because it is too large Load diff

View file

@ -156,31 +156,6 @@
}
}
// Password validation
let passwordRequirements = $derived.by(() => {
if (!password) {
return {
length: false,
lowercase: false,
uppercase: false,
digit: false,
special: false,
};
}
return {
length: password.length >= 8,
lowercase: /[a-z]/.test(password),
uppercase: /[A-Z]/.test(password),
digit: /[0-9]/.test(password),
special: /[^a-zA-Z0-9]/.test(password),
};
});
function getPageBackground() {
return isDark ? darkBackground : lightBackground;
}
async function handleRegister() {
loading = true;
error = null;
@ -266,23 +241,22 @@
<svelte:head>
<title>Create Account - {appName}</title>
<meta name="theme-color" content={darkBackground} media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content={lightBackground} media="(prefers-color-scheme: light)" />
</svelte:head>
<div
class="flex min-h-screen flex-col justify-between"
style="background-color: {getPageBackground()}; max-width: 100vw; overflow-x: hidden;"
class="flex flex-col min-h-screen min-h-dvh w-full max-w-[100vw] overflow-x-hidden m-0 p-0"
style:background-color={isDark ? darkBackground || '#121212' : lightBackground || '#f5f5f5'}
>
<!-- Theme Toggle - Top Left -->
<!-- Theme Toggle -->
<button
type="button"
onclick={toggleTheme}
style="position: absolute; top: 1rem; left: 1rem; z-index: 50; display: flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 0.5rem; border: 1px solid {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.2)'}; background: {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.05)'}; color: {isDark
? 'rgba(255, 255, 255, 0.7)'
: 'rgba(0, 0, 0, 0.7)'}; cursor: pointer; transition: all 0.2s ease;"
class="absolute top-4 left-4 z-50 flex items-center justify-center w-10 h-10 rounded-lg border cursor-pointer transition-all"
style:border-color={isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'}
style:background-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}
style:color={isDark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.7)'}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if isDark}
@ -293,295 +267,345 @@
</button>
{#if headerControls}
<div
style="position: absolute; top: 1rem; right: 1rem; z-index: 50; opacity: 0.6; display: flex; gap: 0.75rem;"
>
<div class="absolute top-4 right-4 z-50 opacity-60 flex gap-3">
{@render headerControls()}
</div>
{/if}
<!-- Top Section - Logo -->
<div class="flex flex-col items-center justify-center pt-16 pb-8">
<div
class="flex items-center justify-center rounded-full transition-all mb-4"
style="width: 120px; height: 120px; border: 3px solid {primaryColor}; background-color: {isDark
? '#000'
: '#fff'}; box-shadow: {isDark
? '0 6px 12px rgba(0, 0, 0, 0.4)'
: '0 6px 12px rgba(0, 0, 0, 0.15)'};"
>
<Logo size={55} color={primaryColor} />
</div>
<h1 class="text-2xl font-semibold" style="color: {isDark ? '#ffffff' : '#000000'};">
{appName}
</h1>
</div>
<!-- Middle Section - Register Form -->
<div class="flex-1 flex items-start justify-center px-5 pt-8 pb-8">
<div
class="w-full max-w-md rounded-xl p-6"
style="background-color: {isDark
? 'rgba(255, 255, 255, 0.08)'
: 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.1)'};"
>
<!-- Title -->
<h2
class="mb-6 text-center text-xl font-semibold"
style="color: {isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)'};"
<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'}
>
{t.title}
</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="mb-4 rounded-xl bg-red-500/20 border border-red-500/30 p-3">
<p class="text-sm text-red-500">{error}</p>
<!-- 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>
</div>
{/if}
<!-- Verification Email Sent Confirmation -->
{#if verificationEmailSent}
<div class="mb-4 rounded-xl bg-green-500/20 border border-green-500/30 p-4">
<p class="text-sm text-green-500 font-medium">
{t.verificationEmailSent}
</p>
</div>
{/if}
<!-- 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}
<!-- Success Message with Resend Option -->
{#if success && needsVerification}
<div
class="mb-6 rounded-xl p-5"
style="background-color: {isDark
? 'rgba(34, 197, 94, 0.15)'
: 'rgba(34, 197, 94, 0.1)'}; border: 2px solid rgba(34, 197, 94, 0.4);"
<!-- 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 -->
{#if success && needsVerification}
<div class="mb-6 rounded-xl p-5 bg-green-500/15 border-2 border-green-500/40">
<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 bg-green-500/20"
>
<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>
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style:color={isDark ? '#22c55e' : '#16a34a'}
>
{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)'}
>
{t.accountCreated}
</p>
</div>
</div>
{#if onResendVerification}
<div
class="pt-3 border-t"
style:border-color={isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}
>
<p
class="text-xs mb-2"
style:color={isDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'}
>
Didn't receive the email?
</p>
<button
type="button"
onclick={handleResendVerification}
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>
</div>
{/if}
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
>
<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: rgba(34, 197, 94, 0.2);"
>
<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>
</div>
<div>
<h3
class="font-semibold text-base mb-1"
style="color: {isDark ? '#22c55e' : '#16a34a'};"
>
{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)'};"
>
{t.accountCreated}
</p>
</div>
<!-- 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>
{#if onResendVerification}
<div
class="pt-3 border-t"
style="border-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
>
<p
class="text-xs mb-2"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
>
Didn't receive the email?
</p>
<!-- 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={handleResendVerification}
disabled={resendingVerification}
class="w-full flex items-center justify-center gap-2 h-11 rounded-lg font-medium transition-all hover:opacity-80 disabled:opacity-50"
style="background-color: {primaryColor}40; border: 1.5px solid {primaryColor}; color: {isDark
? '#ffffff'
: '#000000'};"
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 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}
{#if showPassword}
<EyeSlash size={20} />
{:else}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
></path>
</svg>
{t.resendVerification}
<Eye size={20} />
{/if}
</button>
</div>
{/if}
</div>
{/if}
<!-- Register Form -->
<form
onsubmit={(e) => {
e.preventDefault();
handleRegister();
}}
class="space-y-4"
>
<div>
<input
type="email"
bind:value={email}
placeholder={t.emailPlaceholder}
required
class="h-14 w-full rounded-xl border px-4 text-lg transition-colors focus:outline-none focus:ring-2"
style="background-color: {isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.8)'}; border-color: {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
? '#ffffff'
: '#000000'}; --tw-ring-color: {primaryColor};"
/>
</div>
<div>
<div class="relative">
<input
type={showPassword ? 'text' : 'password'}
bind:value={password}
placeholder={t.passwordPlaceholder}
required
minlength={8}
class="h-14 w-full rounded-xl border px-4 pr-14 text-lg transition-colors focus:outline-none focus:ring-2"
style="background-color: {isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.8)'}; border-color: {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
? '#ffffff'
: '#000000'}; --tw-ring-color: {primaryColor};"
/>
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute inset-y-0 right-0 flex items-center justify-center w-14 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
</div>
</div>
<PasswordStrength {password} {primaryColor} />
<PasswordStrength {password} {primaryColor} />
<div>
<div class="relative">
<input
type={showConfirmPassword ? 'text' : 'password'}
bind:value={confirmPassword}
placeholder={t.confirmPasswordPlaceholder}
required
minlength={8}
class="h-14 w-full rounded-xl border px-4 pr-14 text-lg transition-colors focus:outline-none focus:ring-2"
style="background-color: {isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.8)'}; border-color: {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
? '#ffffff'
: '#000000'}; --tw-ring-color: {primaryColor};"
/>
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute inset-y-0 right-0 flex items-center justify-center w-14 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}
<EyeSlash size={20} />
{:else}
<Eye size={20} />
{/if}
</button>
<!-- 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}
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>
<!-- Password Requirements -->
<p
class="text-xs -mt-2"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
>
{t.passwordRequirements}
</p>
<button
type="submit"
disabled={loading}
class="flex h-14 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2 mt-2"
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark
? '#ffffff'
: '#000000'};"
>
<UserPlus size={20} class="inline-block" />
{loading ? t.creatingAccount : t.createAccountButton}
</button>
</form>
<!-- Back Button -->
<div class="mt-4">
<button
onclick={() => goto(loginPath)}
class="flex h-10 w-full items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80"
style="color: {isDark ? '#ffffff' : '#000000'};"
>
<ArrowLeft size={20} class="inline-block" />
{t.backToLogin}
</button>
</div>
</div>
</div>
</main>
<!-- App Slider -->
{#if appSlider}
<div class="w-full px-4 pb-8">
<footer class="w-full pb-4 anim-fade-in">
{@render appSlider()}
</div>
{:else}
<!-- Bottom padding -->
<div class="pb-8"></div>
</footer>
{/if}
</div>
<style>
:global(html, body) {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.anim-fade-in-up {
animation: fadeInUp 0.5s ease-out 0.15s both;
}
.anim-fade-in-scale {
animation: fadeInScale 0.5s ease-out both;
}
.anim-fade-in {
animation: fadeIn 0.5s ease-out 0.3s both;
}
@media (prefers-reduced-motion: reduce) {
.anim-fade-in-up,
.anim-fade-in-scale,
.anim-fade-in {
animation: none;
}
* {
transition-duration: 0.01ms !important;
}
}
</style>