mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(auth): redesign login page with gradient blobs and glass card
This commit is contained in:
@@ -1,9 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import type { OidcPublicConfig } from '@projectx/types'
|
||||
import { Moon, Sun } from 'lucide-vue-next'
|
||||
import { ACCENT_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { useAuth } from './composables/useAuth'
|
||||
import { useOidc } from './composables/useOidc'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const accentOpen = ref(false)
|
||||
const currentAccent = computed(() => ACCENT_OPTIONS.find((o) => o.id === themeStore.accent))
|
||||
|
||||
const { login } = useAuth()
|
||||
const { getPublicConfig, initiateLogin } = useOidc()
|
||||
|
||||
@@ -43,8 +49,51 @@ async function handleOidcLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<div class="w-full max-w-sm">
|
||||
<div class="login-bg min-h-screen flex items-center justify-center px-4 overflow-hidden">
|
||||
<div class="blob blob-1" />
|
||||
<div class="blob blob-2" />
|
||||
<div class="blob blob-3" />
|
||||
|
||||
<!-- Compact theme picker -->
|
||||
<div class="fixed bottom-5 right-5 z-20 flex items-center gap-1.5">
|
||||
<!-- Dark / light toggle -->
|
||||
<button class="theme-btn" :title="themeStore.theme === 'dark' ? 'Switch to light' : 'Switch to dark'" @click="themeStore.toggleTheme()">
|
||||
<Sun v-if="themeStore.theme === 'dark'" :size="14" />
|
||||
<Moon v-else :size="14" />
|
||||
</button>
|
||||
|
||||
<!-- Accent picker -->
|
||||
<div class="relative">
|
||||
<!-- Colour popover -->
|
||||
<Transition name="popover">
|
||||
<div v-if="accentOpen" class="accent-popover absolute bottom-full right-0 mb-2 p-3 rounded-xl flex items-center gap-3">
|
||||
<button
|
||||
v-for="opt in ACCENT_OPTIONS"
|
||||
:key="opt.id"
|
||||
:title="opt.label"
|
||||
class="w-5 h-5 rounded-full transition-all hover:scale-125 focus:outline-none shrink-0"
|
||||
:style="{
|
||||
backgroundColor: opt.color,
|
||||
outline: themeStore.accent === opt.id ? `2px solid ${opt.color}` : 'none',
|
||||
outlineOffset: '2px',
|
||||
transform: themeStore.accent === opt.id ? 'scale(1.2)' : '',
|
||||
}"
|
||||
@click="themeStore.setAccent(opt.id)"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Swatch button showing current accent -->
|
||||
<button class="theme-btn" title="Change accent color" @click="accentOpen = !accentOpen">
|
||||
<span class="w-3.5 h-3.5 rounded-full block" :style="{ backgroundColor: currentAccent?.color }" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Click-outside backdrop -->
|
||||
<div v-if="accentOpen" class="fixed inset-0 z-10" @click="accentOpen = false" />
|
||||
|
||||
<div class="login-card relative z-10 w-full max-w-sm rounded-2xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-serif font-semibold text-foreground">project<span class="text-primary">x</span></h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">Sign in to your account</p>
|
||||
@@ -59,7 +108,7 @@ async function handleOidcLogin() {
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
required
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
class="w-full rounded-md border border-input bg-background/60 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +120,7 @@ async function handleOidcLogin() {
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
class="w-full rounded-md border border-input bg-background/60 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -87,19 +136,16 @@ async function handleOidcLogin() {
|
||||
</form>
|
||||
|
||||
<template v-if="oidcConfig?.enabled">
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-border" />
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs">
|
||||
<span class="bg-background px-2 text-muted-foreground">or</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 my-6">
|
||||
<div class="flex-1 h-px bg-border" />
|
||||
<span class="text-xs text-muted-foreground">or</span>
|
||||
<div class="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:disabled="oidcLoading"
|
||||
class="w-full rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-50 transition-colors"
|
||||
class="w-full rounded-md border border-input bg-background/60 px-4 py-2 text-sm font-medium text-foreground hover:bg-muted/60 disabled:opacity-50 transition-colors backdrop-blur-sm"
|
||||
@click="handleOidcLogin"
|
||||
>
|
||||
{{ oidcLoading ? 'Redirecting...' : `Sign in with ${oidcConfig.providerName || 'SSO'}` }}
|
||||
@@ -112,3 +158,123 @@ async function handleOidcLogin() {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-bg {
|
||||
background: var(--background);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(90px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 640px;
|
||||
height: 520px;
|
||||
background: color-mix(in oklch, var(--primary) 28%, transparent);
|
||||
top: -12%;
|
||||
left: -18%;
|
||||
animation: drift-1 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 520px;
|
||||
height: 560px;
|
||||
background: color-mix(in oklch, var(--primary) 18%, transparent);
|
||||
bottom: -18%;
|
||||
right: -12%;
|
||||
animation: drift-2 26s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: color-mix(in oklch, var(--primary) 14%, transparent);
|
||||
top: 35%;
|
||||
left: 38%;
|
||||
animation: drift-3 18s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes drift-1 {
|
||||
from {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
to {
|
||||
transform: translate(70px, 55px) scale(1.12);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drift-2 {
|
||||
from {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
to {
|
||||
transform: translate(-55px, -70px) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drift-3 {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-35px, 28px) scale(0.92);
|
||||
}
|
||||
100% {
|
||||
transform: translate(35px, -28px) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 9999px;
|
||||
background: color-mix(in oklch, var(--card) 72%, transparent);
|
||||
border: 1px solid color-mix(in oklch, var(--border) 55%, transparent);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: var(--elevation-md);
|
||||
color: var(--muted-foreground);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.theme-btn:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.accent-popover {
|
||||
background: color-mix(in oklch, var(--card) 80%, transparent);
|
||||
border: 1px solid color-mix(in oklch, var(--border) 55%, transparent);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow: var(--elevation-lg);
|
||||
}
|
||||
|
||||
.popover-enter-active,
|
||||
.popover-leave-active {
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
}
|
||||
|
||||
.popover-enter-from,
|
||||
.popover-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.97);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: color-mix(in oklch, var(--card) 72%, transparent);
|
||||
border: 1px solid color-mix(in oklch, var(--border) 55%, transparent);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow: var(--elevation-xl);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1768360764570,
|
||||
"when": 1768364315691,
|
||||
"tag": "0000_add_oidc_schema",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user