mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 02:41:09 +02:00
feat(shared-auth-ui): redesign login page with animations and theme support
- Migrate icons from custom implementation to @manacore/shared-icons (phosphor-svelte) - Add CSS media queries for dark/light mode (no more flash on reload) - Add subtle entrance animations (logo fadeInScale, form fadeInUp, slider fadeIn) - Redesign custom checkbox with CSS-only styling - Remove isDark prop dependency, use prefers-color-scheme instead - Update auth layout to allow full-page rendering Also updates AppSlider component: - Add staggered entrance animations for app cards - Redesign modal with glassmorphism style - Add theme-aware styling via CSS media queries - Improve status badge design in modal - Add close button in top-right corner 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
61d181fbc2
commit
129692812b
10 changed files with 1083 additions and 603 deletions
|
|
@ -2,8 +2,4 @@
|
|||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{@render children()}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
],
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"@manacore/shared-auth": "workspace:*"
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.16.0",
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { iconPaths, type IconName } from '../icons/iconPaths';
|
||||
|
||||
interface Props {
|
||||
name: IconName;
|
||||
size?: number;
|
||||
class?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { name, size = 24, class: className = '', color }: Props = $props();
|
||||
|
||||
const path = $derived(iconPaths[name]);
|
||||
</script>
|
||||
|
||||
{#if path}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color || 'currentColor'}
|
||||
viewBox="0 0 256 256"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{@html path}
|
||||
</svg>
|
||||
{:else}
|
||||
<span class="text-red-500" title="Icon '{name}' not found">⚠</span>
|
||||
{/if}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/**
|
||||
* Phosphor Icons (Bold weight) SVG paths
|
||||
* Only includes icons used in auth UI
|
||||
*/
|
||||
export const iconPaths = {
|
||||
'user-plus':
|
||||
'<path d="M256,136a8,8,0,0,1-8,8H232v16a8,8,0,0,1-16,0V144H200a8,8,0,0,1,0-16h16V112a8,8,0,0,1,16,0v16h16A8,8,0,0,1,256,136Zm-57.87,58.85a8,8,0,0,1-12.26,10.3C165.75,181.19,138.09,168,108,168s-57.75,13.19-77.87,37.15a8,8,0,0,1-12.26-10.3C32.09,177.09,52.67,163.91,76,157.53a68,68,0,1,1,64,0C163.33,163.91,183.91,177.09,198.13,194.85ZM108,152a52,52,0,1,0-52-52A52.06,52.06,0,0,0,108,152Z"/>',
|
||||
|
||||
'sign-in':
|
||||
'<path d="M144,136v-16a8,8,0,0,0-8-8H48a8,8,0,0,0-8,8v16a8,8,0,0,0,8,8h88A8,8,0,0,0,144,136Zm-27.31,52.69a8,8,0,0,0-11.32,11.31L136,230.63V224a8,8,0,0,0-16,0v24a8,8,0,0,0,8,8h24a8,8,0,0,0,0-16h-6.63l30.32-30.31a8,8,0,0,0-11.31-11.32l-24,24ZM200,32H136a8,8,0,0,0,0,16h64V208H136a8,8,0,0,0,0,16h64a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Z"/>',
|
||||
|
||||
eye: '<path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"/>',
|
||||
|
||||
'eye-off':
|
||||
'<path d="M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.58A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z"/>',
|
||||
|
||||
key: '<path d="M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"/>',
|
||||
|
||||
'arrow-left':
|
||||
'<path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"/>',
|
||||
|
||||
info: '<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/>',
|
||||
|
||||
'mail-open':
|
||||
'<path d="M228.44,89.34l-96-64a8,8,0,0,0-8.88,0l-96,64A8,8,0,0,0,24,96V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V96A8,8,0,0,0,228.44,89.34ZM128,41.61l81.91,54.61-67,47.78a8,8,0,0,1-9.79,0l-67-47.78ZM40,200V111.53l65.9,47a24,24,0,0,0,29.44,0l65.9-47L216,200Z"/>',
|
||||
|
||||
lock: '<path d="M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"/>',
|
||||
|
||||
'shield-check':
|
||||
'<path d="M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,26.07,47.48,26.53a8,8,0,0,0,5.18,0c1.52-.46,24.42-7.67,47.48-26.53C200.48,196.67,226,164.72,226,112V56A16,16,0,0,0,208,40Zm0,72c0,37.07-13.66,67.16-40.58,89.42A132.87,132.87,0,0,1,128,224.54a132.77,132.77,0,0,1-39.42-23.12C61.66,179.16,48,149.07,48,112V56H208ZM82.34,141.66a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32l-56,56a8,8,0,0,1-11.32,0Z"/>',
|
||||
|
||||
'arrows-left-right':
|
||||
'<path d="M45.66,77.66A8,8,0,0,1,40,64h80a8,8,0,0,1,0,16H59.31l18.35,18.34a8,8,0,0,1-11.32,11.32ZM216,176H136a8,8,0,0,0,0,16h60.69l-18.35,18.34a8,8,0,0,0,11.32,11.32l32-32a8,8,0,0,0,0-11.32l-32-32a8,8,0,0,0-11.32,11.32L196.69,176Z"/>',
|
||||
|
||||
envelope:
|
||||
'<path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/>',
|
||||
|
||||
folder:
|
||||
'<path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"/>',
|
||||
|
||||
music:
|
||||
'<path d="M212.92,17.69a8,8,0,0,0-6.86-1.45l-128,32A8,8,0,0,0,72,56V166.09A36,36,0,1,0,88,196V62.25l112-28v99.84A36,36,0,1,0,216,168V24A8,8,0,0,0,212.92,17.69ZM52,216a20,20,0,1,1,20-20A20,20,0,0,1,52,216Zm128-32a20,20,0,1,1,20-20A20,20,0,0,1,180,184Z"/>',
|
||||
|
||||
refresh:
|
||||
'<path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64A80,80,0,1,0,128,208a8,8,0,0,1,0,16A96,96,0,1,1,195.26,60.49L224,85.34V56a8,8,0,0,1,16,0Z"/>',
|
||||
|
||||
check:
|
||||
'<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"/>',
|
||||
|
||||
warning:
|
||||
'<path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/>',
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof iconPaths;
|
||||
|
|
@ -4,7 +4,6 @@ export { default as RegisterPage } from './pages/RegisterPage.svelte';
|
|||
export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte';
|
||||
|
||||
// Components
|
||||
export { default as Icon } from './components/Icon.svelte';
|
||||
export { default as GoogleSignInButton } from './components/GoogleSignInButton.svelte';
|
||||
export { default as AppleSignInButton } from './components/AppleSignInButton.svelte';
|
||||
|
||||
|
|
@ -30,12 +29,9 @@ export {
|
|||
} from './utils/appleAuth';
|
||||
|
||||
// Types
|
||||
export type { AuthUIConfig, AuthServiceInterface, AuthResult, IconName } from './types';
|
||||
export type { AuthUIConfig, AuthServiceInterface, AuthResult } from './types';
|
||||
|
||||
// Page Translation Types
|
||||
export type { LoginTranslations } from './pages/LoginPage.svelte';
|
||||
export type { RegisterTranslations } from './pages/RegisterPage.svelte';
|
||||
export type { ForgotPasswordTranslations } from './pages/ForgotPasswordPage.svelte';
|
||||
|
||||
// Icon paths
|
||||
export { iconPaths } from './icons/iconPaths';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import type { AuthResult } from '../types';
|
||||
import Icon from '../components/Icon.svelte';
|
||||
import { Key, ArrowLeft, EnvelopeOpen, SignIn } from '@manacore/shared-icons';
|
||||
|
||||
type PageMode = 'form' | 'success';
|
||||
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
style="background-color: {getPageBackground()}; max-width: 100vw; overflow-x: hidden;"
|
||||
>
|
||||
<!-- Top Section - Logo -->
|
||||
<div class="flex flex-col items-center justify-center pt-16 pb-8">
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
? '#ffffff'
|
||||
: '#000000'};"
|
||||
>
|
||||
<Icon name="key" size={20} />
|
||||
<Key size={20} class="inline-block" />
|
||||
{loading ? t.sending : t.sendResetLinkButton}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -233,7 +233,7 @@
|
|||
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'};"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
<ArrowLeft size={20} class="inline-block" />
|
||||
{t.backToLogin}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -244,9 +244,9 @@
|
|||
<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="background-color: {primaryColor}30; color: {primaryColor};"
|
||||
>
|
||||
<Icon name="mail-open" size={40} color={primaryColor} />
|
||||
<EnvelopeOpen size={40} />
|
||||
</div>
|
||||
|
||||
<p
|
||||
|
|
@ -268,7 +268,7 @@
|
|||
? '#ffffff'
|
||||
: '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
<SignIn size={20} class="inline-block" />
|
||||
{t.backToLogin}
|
||||
</button>
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import type { AuthResult } from '../types';
|
||||
import Icon from '../components/Icon.svelte';
|
||||
import { Eye, EyeSlash, UserPlus, ArrowLeft } from '@manacore/shared-icons';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
|
|
@ -218,7 +218,7 @@
|
|||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
style="background-color: {getPageBackground()}; max-width: 100vw; overflow-x: hidden;"
|
||||
>
|
||||
<!-- Top Section - Logo -->
|
||||
<div class="flex flex-col items-center justify-center pt-16 pb-8">
|
||||
|
|
@ -296,64 +296,68 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 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-12 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 right-3 top-1/2 -translate-y-1/2 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label={showPassword ? t.hidePassword : t.showPassword}
|
||||
>
|
||||
<Icon
|
||||
name={showPassword ? 'eye-off' : 'eye'}
|
||||
size={20}
|
||||
color={isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'}
|
||||
<div class="mb-2">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div class="mb-2 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-12 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 right-3 top-1/2 -translate-y-1/2 p-2 rounded-lg hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
|
||||
>
|
||||
<Icon
|
||||
name={showConfirmPassword ? 'eye-off' : 'eye'}
|
||||
size={20}
|
||||
color={isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'}
|
||||
<div class="mb-2">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Requirements -->
|
||||
|
|
@ -372,7 +376,7 @@
|
|||
? '#ffffff'
|
||||
: '#000000'};"
|
||||
>
|
||||
<Icon name="user-plus" size={20} />
|
||||
<UserPlus size={20} class="inline-block" />
|
||||
{loading ? t.creatingAccount : t.createAccountButton}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -384,7 +388,7 @@
|
|||
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'};"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
<ArrowLeft size={20} class="inline-block" />
|
||||
{t.backToLogin}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -61,20 +61,3 @@ export interface AuthResult {
|
|||
needsVerification?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon names available in the icon set
|
||||
*/
|
||||
export type IconName =
|
||||
| 'user-plus'
|
||||
| 'sign-in'
|
||||
| 'eye'
|
||||
| 'eye-off'
|
||||
| 'key'
|
||||
| 'arrow-left'
|
||||
| 'info'
|
||||
| 'mail-open'
|
||||
| 'lock'
|
||||
| 'shield-check'
|
||||
| 'arrows-left-right'
|
||||
| 'envelope'
|
||||
| 'folder';
|
||||
|
|
|
|||
|
|
@ -98,59 +98,35 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<h3
|
||||
class="mb-4 text-center text-sm font-medium"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div class="app-slider-root">
|
||||
<h3 class="slider-title">{title}</h3>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4"
|
||||
style="perspective: 1000px;"
|
||||
>
|
||||
<div class="slider-scroll-container">
|
||||
<div class="slider-items">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="group relative flex-shrink-0 rounded-xl p-5 cursor-pointer snap-center transition-transform hover:scale-105"
|
||||
style="width: 160px; 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)'};"
|
||||
class="app-card"
|
||||
style="--index: {index};"
|
||||
onmouseenter={() => (hoveredApp = index)}
|
||||
onmouseleave={() => (hoveredApp = null)}
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<div
|
||||
class="absolute top-3 right-3 w-3 h-3 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(
|
||||
app.status
|
||||
)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
|
||||
class="status-indicator"
|
||||
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="mb-2 flex h-20 w-20 mx-auto items-center justify-center rounded-xl transition-transform group-hover:scale-110"
|
||||
>
|
||||
<div class="app-icon-wrapper">
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="w-16 h-16 object-contain" />
|
||||
<img src={app.icon} alt={app.name} class="app-icon" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded font-bold text-lg"
|
||||
style="color: {app.color};"
|
||||
>
|
||||
<div class="app-icon-fallback" style="color: {app.color};">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h4
|
||||
class="text-base font-semibold text-center"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
{app.name}
|
||||
</h4>
|
||||
<h4 class="app-name">{app.name}</h4>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -159,8 +135,7 @@
|
|||
|
||||
{#if selectedApp !== null}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.85);"
|
||||
class="modal-overlay"
|
||||
onclick={closeModal}
|
||||
onkeydown={(e) => e.key === 'Escape' && closeModal()}
|
||||
role="dialog"
|
||||
|
|
@ -169,37 +144,24 @@
|
|||
>
|
||||
<button
|
||||
onclick={closeModal}
|
||||
class="absolute top-6 right-6 rounded-full p-2 transition-all hover:bg-white/10 z-10"
|
||||
class="modal-close-btn"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
bind:this={modalScrollContainer}
|
||||
class="absolute inset-0 flex items-center overflow-x-auto scrollbar-hide snap-x snap-mandatory scroll-smooth"
|
||||
class="modal-scroll-container scrollbar-hide"
|
||||
>
|
||||
<div class="flex gap-6 px-8 py-8 mx-auto" style="perspective: 1000px;">
|
||||
<div class="modal-cards-wrapper">
|
||||
{#each apps as app, index}
|
||||
<div
|
||||
class="flex-shrink-0 rounded-3xl p-8 snap-center shadow-2xl relative"
|
||||
style="min-width: 360px; max-width: 360px; background-color: {hoveredApp === index
|
||||
? isDark
|
||||
? '#2A2A2A'
|
||||
: '#F5F5F5'
|
||||
: isDark
|
||||
? '#1E1E1E'
|
||||
: '#ffffff'}; border: 3px solid {app.color}40; transform: perspective(1000px) rotateX({cardRotations[
|
||||
index
|
||||
]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY ||
|
||||
0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
|
||||
class="modal-card"
|
||||
class:active={selectedApp === index}
|
||||
style="transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg);"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectedApp = index;
|
||||
|
|
@ -214,66 +176,37 @@
|
|||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="absolute top-4 right-4 flex items-center gap-2">
|
||||
<span
|
||||
class="text-xs font-medium"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
{getStatusLabel(app.status)}
|
||||
</span>
|
||||
<div class="modal-card-status">
|
||||
<div
|
||||
class="w-4 h-4 rounded-full status-indicator"
|
||||
style="background-color: {getStatusColor(
|
||||
app.status
|
||||
)}; box-shadow: 0 0 12px {getStatusColor(app.status)};"
|
||||
class="modal-status-dot"
|
||||
style="background-color: {getStatusColor(app.status)};"
|
||||
></div>
|
||||
<span class="modal-status-label">{getStatusLabel(app.status)}</span>
|
||||
</div>
|
||||
|
||||
{#if app.icon}
|
||||
<img src={app.icon} alt={app.name} class="w-28 h-28 object-contain mb-3 mx-auto" />
|
||||
<img src={app.icon} alt={app.name} class="modal-app-icon" />
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded font-bold text-3xl mb-3 mx-auto"
|
||||
style="color: {app.color};"
|
||||
>
|
||||
<div class="modal-app-icon-fallback" style="color: {app.color};">
|
||||
{app.name.charAt(0)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3
|
||||
class="text-2xl font-bold mb-2 text-center"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
{app.name}
|
||||
</h3>
|
||||
<h3 class="modal-app-name">{app.name}</h3>
|
||||
|
||||
<p class="text-sm mb-4 text-center font-medium" style="color: {app.color};">
|
||||
<p class="modal-app-tagline" style="color: {app.color};">
|
||||
{app.description}
|
||||
</p>
|
||||
|
||||
<p
|
||||
class="text-sm leading-relaxed mb-6 text-center"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
{app.longDescription}
|
||||
</p>
|
||||
<p class="modal-app-description">{app.longDescription}</p>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="modal-app-action">
|
||||
{#if app.comingSoon}
|
||||
<div
|
||||
class="inline-block rounded-full px-5 py-2.5 text-sm font-medium"
|
||||
style="background-color: {isDark
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
|
||||
? 'rgba(255, 255, 255, 0.5)'
|
||||
: 'rgba(0, 0, 0, 0.5)'};"
|
||||
>
|
||||
{comingSoonLabel}
|
||||
</div>
|
||||
<span class="modal-coming-soon">{comingSoonLabel}</span>
|
||||
{:else}
|
||||
<button
|
||||
class="rounded-xl px-8 py-3 text-sm font-semibold transition-all hover:opacity-80 border-2 text-white"
|
||||
style="background-color: {app.color}60; border-color: {app.color};"
|
||||
class="modal-open-btn"
|
||||
style="background-color: {app.color}40; border-color: {app.color};"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAppAction(app, index);
|
||||
|
|
@ -291,6 +224,163 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.app-slider-root {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.slider-title {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.slider-title {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.slider-scroll-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.slider-scroll-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider-items {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
padding-bottom: 1.5rem;
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
padding: 1.25rem 1rem;
|
||||
border-radius: 1rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
/* Staggered entrance animation */
|
||||
animation: fadeInUp 0.4s ease-out both;
|
||||
animation-delay: calc(0.3s + var(--index) * 0.08s);
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.app-card {
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.app-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.app-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto 0.75rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.app-card:hover .app-icon-wrapper {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-icon-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.app-name {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Entrance animation */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app-card {
|
||||
animation: none;
|
||||
}
|
||||
.status-indicator {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -300,17 +390,244 @@
|
|||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
.modal-close-btn {
|
||||
position: fixed;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
z-index: 60;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.modal-scroll-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.modal-cards-wrapper {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
flex-shrink: 0;
|
||||
width: 340px;
|
||||
padding: 2rem;
|
||||
padding-top: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
scroll-snap-align: center;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.1s ease-out, background-color 0.2s ease;
|
||||
animation: modalCardIn 0.3s ease-out both;
|
||||
}
|
||||
|
||||
.modal-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-card {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
.modal-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-card-status {
|
||||
position: absolute;
|
||||
top: 0.875rem;
|
||||
right: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.625rem 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-card-status {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-status-dot {
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-status-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-status-label {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-app-icon {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
object-fit: contain;
|
||||
margin: 0 auto 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-app-icon-fallback {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-app-name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-app-name {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-app-tagline {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.modal-app-description {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin: 0 0 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-app-description {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-app-action {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-coming-soon {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-coming-soon {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-open-btn {
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-open-btn:hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.modal-open-btn {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes modalCardIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.modal-overlay,
|
||||
.modal-card {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue