mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat: add i18n localization with language switcher to all web apps
- Add svelte-i18n configuration with SSR support to all web apps - Create LanguageSelector component for each app with brand colors - Add German and English locale files - Integrate language switcher into login pages via headerControls snippet - Fix Tailwind v4 @source directives for shared package scanning - Update AppSlider styling to match login container design Apps updated: - Memoro (gold #f8d62b) - Märchenzauber (pink #FF6B9D) - ManaDeck (purple #8b5cf6) - ManaCore (indigo #6366f1) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
bd869dfe09
commit
926ca231b5
147 changed files with 7090 additions and 2276 deletions
|
|
@ -10,7 +10,8 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ export const iconPaths = {
|
|||
|
||||
'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"/>'
|
||||
'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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Pages
|
||||
export { default as LoginPage } from './pages/LoginPage.svelte';
|
||||
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';
|
||||
|
|
|
|||
239
packages/shared-auth-ui/src/pages/ForgotPasswordPage.svelte
Normal file
239
packages/shared-auth-ui/src/pages/ForgotPasswordPage.svelte
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<script lang="ts">
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import type { AuthResult } from '../types';
|
||||
import Icon from '../components/Icon.svelte';
|
||||
|
||||
type PageMode = 'form' | 'success';
|
||||
|
||||
interface Props {
|
||||
/** App name */
|
||||
appName: string;
|
||||
/** Logo component */
|
||||
logo: Component<{ size?: number; color?: string }>;
|
||||
/** Primary color (hex) */
|
||||
primaryColor: string;
|
||||
/** Forgot password function */
|
||||
onForgotPassword: (email: string) => Promise<AuthResult>;
|
||||
/** Navigation function */
|
||||
goto: (path: string) => void;
|
||||
/** Login page path */
|
||||
loginPath?: string;
|
||||
/** Light background color */
|
||||
lightBackground?: string;
|
||||
/** Dark background color */
|
||||
darkBackground?: string;
|
||||
/** App slider snippet */
|
||||
appSlider?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
appName,
|
||||
logo: Logo,
|
||||
primaryColor,
|
||||
onForgotPassword,
|
||||
goto,
|
||||
loginPath = '/login',
|
||||
lightBackground = '#f5f5f5',
|
||||
darkBackground = '#121212',
|
||||
appSlider
|
||||
}: Props = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let email = $state('');
|
||||
let mode = $state<PageMode>('form');
|
||||
let resetEmail = $state('');
|
||||
|
||||
// Check for dark mode
|
||||
let isDark = $state(false);
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
isDark = e.matches;
|
||||
};
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', listener);
|
||||
return () => {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', listener);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function getPageBackground() {
|
||||
return isDark ? darkBackground : lightBackground;
|
||||
}
|
||||
|
||||
async function handleForgotPassword() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
if (!email) {
|
||||
error = 'Email is required';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onForgotPassword(email);
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
resetEmail = email;
|
||||
email = '';
|
||||
mode = 'success';
|
||||
} else {
|
||||
error = result.error || 'Failed to send reset email';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Forgot Password - {appName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
>
|
||||
<!-- 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 - 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)'};"
|
||||
>
|
||||
{mode === 'form' ? 'Reset Password' : 'Email Sent'}
|
||||
</h2>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Form Mode -->
|
||||
{#if mode === 'form'}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleForgotPassword();
|
||||
}}
|
||||
class="pb-4"
|
||||
>
|
||||
<p
|
||||
class="mb-4 text-sm"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="Email"
|
||||
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>
|
||||
|
||||
<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"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="key" size={20} />
|
||||
{loading ? 'Sending...' : 'Send Reset Link'}
|
||||
</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'};"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
Back to Login
|
||||
</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;"
|
||||
>
|
||||
<Icon name="mail-open" size={40} color={primaryColor} />
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-sm text-center px-2"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
We've sent a password reset link to <strong>{resetEmail}</strong>. Please check your
|
||||
inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => goto(loginPath)}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border-2"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
Back to Login
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => {
|
||||
mode = 'form';
|
||||
error = null;
|
||||
}}
|
||||
class="flex h-10 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : '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'};"
|
||||
>
|
||||
Resend Email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App Slider -->
|
||||
{#if appSlider}
|
||||
<div class="w-full px-4 pb-8">
|
||||
{@render appSlider()}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Bottom padding -->
|
||||
<div class="pb-8"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -5,8 +5,6 @@
|
|||
import GoogleSignInButton from '../components/GoogleSignInButton.svelte';
|
||||
import AppleSignInButton from '../components/AppleSignInButton.svelte';
|
||||
|
||||
type AuthMode = 'initial' | 'login' | 'forgot-password' | 'password-reset-success';
|
||||
|
||||
interface Props {
|
||||
/** App name */
|
||||
appName: string;
|
||||
|
|
@ -20,8 +18,6 @@
|
|||
onSignInWithGoogle?: (idToken: string) => Promise<AuthResult>;
|
||||
/** Sign in with Apple function */
|
||||
onSignInWithApple?: (identityToken: string) => Promise<AuthResult>;
|
||||
/** Forgot password function */
|
||||
onForgotPassword: (email: string) => Promise<AuthResult>;
|
||||
/** Navigation function */
|
||||
goto: (path: string) => void;
|
||||
/** Enable Google Sign-In */
|
||||
|
|
@ -32,6 +28,8 @@
|
|||
successRedirect?: string;
|
||||
/** Register page path */
|
||||
registerPath?: string;
|
||||
/** Forgot password page path */
|
||||
forgotPasswordPath?: string;
|
||||
/** Light background color */
|
||||
lightBackground?: string;
|
||||
/** Dark background color */
|
||||
|
|
@ -49,12 +47,12 @@
|
|||
onSignIn,
|
||||
onSignInWithGoogle,
|
||||
onSignInWithApple,
|
||||
onForgotPassword,
|
||||
goto,
|
||||
enableGoogle = false,
|
||||
enableApple = false,
|
||||
successRedirect = '/dashboard',
|
||||
registerPath = '/register',
|
||||
forgotPasswordPath = '/forgot-password',
|
||||
lightBackground = '#f5f5f5',
|
||||
darkBackground = '#121212',
|
||||
appSlider,
|
||||
|
|
@ -63,11 +61,16 @@
|
|||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let errorField = $state<'email' | 'password' | 'general' | null>(null);
|
||||
let email = $state('');
|
||||
let password = $state('');
|
||||
let mode = $state<AuthMode>('initial');
|
||||
let resetEmail = $state('');
|
||||
let showPassword = $state(false);
|
||||
let rememberMe = $state(false);
|
||||
let showSuccess = $state(false);
|
||||
let shakeError = $state(false);
|
||||
let emailInput: HTMLInputElement;
|
||||
let passwordInput: HTMLInputElement;
|
||||
let successAnnouncement = $state('');
|
||||
|
||||
// Check for dark mode
|
||||
let isDark = $state(false);
|
||||
|
|
@ -84,22 +87,67 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Autofocus email field on mount
|
||||
$effect(() => {
|
||||
if (emailInput) {
|
||||
emailInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function getPageBackground() {
|
||||
return isDark ? darkBackground : lightBackground;
|
||||
}
|
||||
|
||||
function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function triggerErrorShake() {
|
||||
shakeError = true;
|
||||
setTimeout(() => {
|
||||
shakeError = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function setError(message: string, field: 'email' | 'password' | 'general' = 'general') {
|
||||
error = message;
|
||||
errorField = field;
|
||||
triggerErrorShake();
|
||||
|
||||
// Focus the problematic field for better accessibility
|
||||
setTimeout(() => {
|
||||
if (field === 'email' && emailInput) {
|
||||
emailInput.focus();
|
||||
} else if (field === 'password' && passwordInput) {
|
||||
passwordInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error = null;
|
||||
errorField = null;
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
loading = true;
|
||||
error = null;
|
||||
clearError();
|
||||
|
||||
if (!email) {
|
||||
error = 'Email is required';
|
||||
setError('Email is required', 'email');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
setError('Please enter a valid email address', 'email');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
error = 'Password is required';
|
||||
setError('Password is required', 'password');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -109,55 +157,41 @@
|
|||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
goto(successRedirect);
|
||||
// Show success feedback before redirect
|
||||
showSuccess = true;
|
||||
successAnnouncement = 'Successfully signed in. Redirecting...';
|
||||
setTimeout(() => {
|
||||
goto(successRedirect);
|
||||
}, 600);
|
||||
} else {
|
||||
error = result.error || 'Sign in failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleForgotPassword() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
if (!email) {
|
||||
error = 'Email is required';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await onForgotPassword(email);
|
||||
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
resetEmail = email;
|
||||
resetForm();
|
||||
switchMode('password-reset-success');
|
||||
} else {
|
||||
error = result.error || 'Failed to send reset email';
|
||||
setError(result.error || 'Sign in failed', 'general');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGoogleSuccess(idToken: string) {
|
||||
if (!onSignInWithGoogle) return;
|
||||
|
||||
loading = true;
|
||||
clearError();
|
||||
|
||||
const result = await onSignInWithGoogle(idToken);
|
||||
loading = false;
|
||||
|
||||
if (result.success) {
|
||||
goto(successRedirect);
|
||||
showSuccess = true;
|
||||
successAnnouncement = 'Successfully signed in with Google. Redirecting...';
|
||||
setTimeout(() => {
|
||||
goto(successRedirect);
|
||||
}, 600);
|
||||
} else {
|
||||
error = result.error || 'Google sign in failed';
|
||||
setError(result.error || 'Google sign in failed', 'general');
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
email = '';
|
||||
password = '';
|
||||
error = null;
|
||||
}
|
||||
|
||||
function switchMode(newMode: AuthMode) {
|
||||
mode = newMode;
|
||||
error = null;
|
||||
function skipToForm() {
|
||||
if (emailInput) {
|
||||
emailInput.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -165,6 +199,100 @@
|
|||
<title>Login - {appName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes success-pulse {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.9; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.success-pulse {
|
||||
animation: success-pulse 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.shake,
|
||||
.spinner,
|
||||
.success-pulse {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
* {
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* Skip link styling */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
z-index: 100;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Ensure minimum touch target size (44x44px) */
|
||||
.touch-target {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Skip Link for keyboard users -->
|
||||
<button
|
||||
class="skip-link"
|
||||
onclick={skipToForm}
|
||||
type="button"
|
||||
>
|
||||
Skip to login form
|
||||
</button>
|
||||
|
||||
<!-- Screen reader announcements -->
|
||||
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
||||
{successAnnouncement}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen flex-col justify-between"
|
||||
style="background-color: {getPageBackground()};"
|
||||
|
|
@ -176,115 +304,116 @@
|
|||
</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} />
|
||||
<main>
|
||||
<!-- 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"
|
||||
class:success-pulse={showSuccess}
|
||||
style="width: 120px; height: 120px; border: 3px solid {showSuccess ? '#22c55e' : 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)'};"
|
||||
role="img"
|
||||
aria-label="{appName} logo"
|
||||
>
|
||||
{#if showSuccess}
|
||||
<Icon name="check" size={55} color="#22c55e" />
|
||||
{:else}
|
||||
<Logo size={55} color={primaryColor} />
|
||||
{/if}
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold" style="color: {isDark ? '#ffffff' : '#000000'};">
|
||||
{appName}
|
||||
</h1>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold" style="color: {isDark ? '#ffffff' : '#000000'};">
|
||||
{appName}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Middle Section - Auth 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 -->
|
||||
<div class="mb-6">
|
||||
<h2
|
||||
class="text-center text-xl font-semibold flex items-center justify-center gap-2"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)'};"
|
||||
>
|
||||
{#if mode === 'initial'}
|
||||
Mana Login
|
||||
{:else if mode === 'login'}
|
||||
<!-- Middle Section - Auth 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"
|
||||
class:shake={shakeError}
|
||||
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 -->
|
||||
<div class="mb-6">
|
||||
<h2
|
||||
class="text-center text-xl font-semibold"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.9)'};"
|
||||
>
|
||||
Sign In
|
||||
{:else if mode === 'forgot-password'}
|
||||
Reset Password
|
||||
{:else if mode === 'password-reset-success'}
|
||||
Email Sent
|
||||
{/if}
|
||||
</h2>
|
||||
{#if mode === 'initial'}
|
||||
</h2>
|
||||
<p
|
||||
class="mt-3 text-sm text-center"
|
||||
class="mt-2 text-sm text-center"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};"
|
||||
>
|
||||
Sign in with your Mana account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{#if error}
|
||||
<div
|
||||
id="form-error"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
class="mb-4 rounded-xl bg-red-500/20 border border-red-500/30 p-3 flex items-center gap-2"
|
||||
>
|
||||
<Icon name="warning" size={18} color="#ef4444" />
|
||||
<p class="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</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>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Initial Mode -->
|
||||
{#if mode === 'initial'}
|
||||
<div class="mb-2 flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => goto(registerPath)}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border-2"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="user-plus" size={20} />
|
||||
Create Account
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => switchMode('login')}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : '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'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Mode -->
|
||||
{:else if mode === 'login'}
|
||||
<!-- Login Form -->
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
class="pb-4"
|
||||
aria-busy={loading}
|
||||
aria-describedby={error ? 'form-error' : undefined}
|
||||
>
|
||||
<div class="mb-2">
|
||||
<!-- Email Field -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="sr-only">Email address</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:this={emailInput}
|
||||
bind:value={email}
|
||||
placeholder="Email"
|
||||
required
|
||||
autocomplete="email"
|
||||
aria-invalid={errorField === 'email'}
|
||||
aria-describedby={errorField === 'email' ? 'form-error' : undefined}
|
||||
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};"
|
||||
style="background-color: {isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.8)'}; border-color: {errorField === 'email' ? '#ef4444' : (isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)')}; color: {isDark ? '#ffffff' : '#000000'}; --tw-ring-color: {errorField === 'email' ? '#ef4444' : primaryColor};"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 relative">
|
||||
<!-- Password Field -->
|
||||
<div class="mb-3 relative">
|
||||
<label for="password" class="sr-only">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
bind:this={passwordInput}
|
||||
bind:value={password}
|
||||
placeholder="Password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
aria-invalid={errorField === 'password'}
|
||||
aria-describedby={errorField === 'password' ? 'form-error' : undefined}
|
||||
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};"
|
||||
style="background-color: {isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.8)'}; border-color: {errorField === 'password' ? '#ef4444' : (isDark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)')}; color: {isDark ? '#ffffff' : '#000000'}; --tw-ring-color: {errorField === 'password' ? '#ef4444' : 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"
|
||||
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 touch-target flex items-center justify-center"
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
aria-pressed={showPassword}
|
||||
title={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
<Icon
|
||||
name={showPassword ? 'eye-off' : 'eye'}
|
||||
|
|
@ -294,35 +423,73 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => switchMode('forgot-password')}
|
||||
class="mb-4 flex h-10 w-full items-center justify-center rounded-xl font-medium transition-all hover:opacity-80 border"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : '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'};"
|
||||
>
|
||||
Forgot Password?
|
||||
</button>
|
||||
<!-- Remember Me & Forgot Password Row -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<label class="flex items-center gap-2 cursor-pointer touch-target">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={rememberMe}
|
||||
class="w-5 h-5 rounded border-2 transition-colors cursor-pointer"
|
||||
style="accent-color: {primaryColor};"
|
||||
/>
|
||||
<span
|
||||
class="text-sm"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
Remember me
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => goto(forgotPasswordPath)}
|
||||
class="text-sm font-medium transition-opacity hover:opacity-70 touch-target flex items-center justify-center px-2"
|
||||
style="color: {primaryColor};"
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<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"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
disabled={loading || showSuccess}
|
||||
aria-disabled={loading || showSuccess}
|
||||
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 touch-target"
|
||||
style="background-color: {showSuccess ? '#22c55e' : primaryColor + '60'}; border-color: {showSuccess ? '#22c55e' : primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
{#if loading}
|
||||
<svg
|
||||
class="spinner w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>Signing in...</span>
|
||||
{:else if showSuccess}
|
||||
<Icon name="check" size={20} />
|
||||
<span>Success!</span>
|
||||
{:else}
|
||||
<Icon name="sign-in" size={20} />
|
||||
<span>Sign In</span>
|
||||
{/if}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Social Login -->
|
||||
{#if enableGoogle || enableApple}
|
||||
<div class="my-4 flex items-center gap-3">
|
||||
<div class="my-4 flex items-center gap-3" role="separator" aria-orientation="horizontal">
|
||||
<div class="flex-1 border-t" style="border-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"></div>
|
||||
<p class="text-xs" style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};">or</p>
|
||||
<span class="text-xs" style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};">or</span>
|
||||
<div class="flex-1 border-t" style="border-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-col gap-2">
|
||||
<div class="mb-4 flex flex-col gap-2" role="group" aria-label="Social login options">
|
||||
{#if enableGoogle && onSignInWithGoogle}
|
||||
<GoogleSignInButton onSuccess={handleGoogleSuccess} />
|
||||
{/if}
|
||||
|
|
@ -332,122 +499,29 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Back Button -->
|
||||
<div class="mt-4">
|
||||
<button
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
switchMode('initial');
|
||||
}}
|
||||
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} />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Forgot Password Mode -->
|
||||
{:else if mode === 'forgot-password'}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleForgotPassword();
|
||||
}}
|
||||
class="pb-4"
|
||||
>
|
||||
<!-- Register Link -->
|
||||
<div class="mt-4 text-center">
|
||||
<p
|
||||
class="mb-4 text-sm"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
class="text-sm"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};"
|
||||
>
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="Email"
|
||||
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 class="flex flex-col gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 disabled:opacity-50 border-2"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="key" size={20} />
|
||||
{loading ? 'Sending...' : 'Reset Password'}
|
||||
</button>
|
||||
|
||||
Don't have an account?
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
resetForm();
|
||||
switchMode('login');
|
||||
}}
|
||||
class="flex h-10 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80"
|
||||
style="color: {isDark ? '#ffffff' : '#000000'};"
|
||||
onclick={() => goto(registerPath)}
|
||||
class="font-medium transition-opacity hover:opacity-70 touch-target inline-flex items-center justify-center px-1"
|
||||
style="color: {primaryColor};"
|
||||
>
|
||||
<Icon name="arrow-left" size={20} />
|
||||
Back
|
||||
Create one
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Password Reset Success Mode -->
|
||||
{:else if mode === 'password-reset-success'}
|
||||
<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;"
|
||||
>
|
||||
<Icon name="mail-open" size={40} color={primaryColor} />
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-sm text-center px-2"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
|
||||
>
|
||||
We've sent a password reset link to <strong>{resetEmail}</strong>. Please check your
|
||||
inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
onclick={() => {
|
||||
resetEmail = '';
|
||||
switchMode('login');
|
||||
}}
|
||||
class="flex h-14 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border-2"
|
||||
style="background-color: {primaryColor}60; border-color: {primaryColor}; color: {isDark ? '#ffffff' : '#000000'};"
|
||||
>
|
||||
<Icon name="sign-in" size={20} />
|
||||
Back to Login
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => switchMode('forgot-password')}
|
||||
class="flex h-10 items-center justify-center gap-2 rounded-xl font-medium transition-all hover:opacity-80 border"
|
||||
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : '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'};"
|
||||
>
|
||||
Resend Email
|
||||
</button>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- App Slider (shown on initial mode) -->
|
||||
{#if appSlider && mode === 'initial'}
|
||||
<!-- App Slider -->
|
||||
{#if appSlider}
|
||||
<div class="w-full pb-8 px-2 pt-4">
|
||||
{@render appSlider()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import type { AuthResult } from '../types';
|
||||
import Icon from '../components/Icon.svelte';
|
||||
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
/** App name */
|
||||
appName: string;
|
||||
|
|
@ -22,6 +24,8 @@
|
|||
lightBackground?: string;
|
||||
/** Dark background color */
|
||||
darkBackground?: string;
|
||||
/** App slider snippet */
|
||||
appSlider?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -33,7 +37,8 @@
|
|||
successRedirect = '/dashboard',
|
||||
loginPath = '/login',
|
||||
lightBackground = '#f5f5f5',
|
||||
darkBackground = '#121212'
|
||||
darkBackground = '#121212',
|
||||
appSlider
|
||||
}: Props = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
|
|
@ -305,6 +310,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom padding -->
|
||||
<div class="pb-8"></div>
|
||||
<!-- App Slider -->
|
||||
{#if appSlider}
|
||||
<div class="w-full px-4 pb-8">
|
||||
{@render appSlider()}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Bottom padding -->
|
||||
<div class="pb-8"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
<path
|
||||
d={branding.logoPath}
|
||||
fill={fillColor}
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -119,6 +119,10 @@ export const iconPaths = {
|
|||
gear: '<path d="M128,76a52,52,0,1,0,52,52A52.06,52.06,0,0,0,128,76Zm0,80a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm92-28a92.11,92.11,0,0,1-2.33,20.84A12,12,0,0,1,206.5,158l-16.29-6.73a68.35,68.35,0,0,1-24.91,14.4L163.37,182a12,12,0,0,1-10.06,9.89,92.75,92.75,0,0,1-25.44,0A12,12,0,0,1,117.81,182l-1.93-16.29a68.35,68.35,0,0,1-24.91-14.4L74.68,158a12,12,0,0,1-11.17-9.16,92.11,92.11,0,0,1,0-41.68A12,12,0,0,1,74.68,98l16.29,6.73a68.35,68.35,0,0,1,24.91-14.4L117.81,74a12,12,0,0,1,10.06-9.89,92.75,92.75,0,0,1,25.44,0A12,12,0,0,1,163.37,74l1.93,16.29a68.35,68.35,0,0,1,24.91,14.4L206.5,98a12,12,0,0,1,11.17,9.16A92.11,92.11,0,0,1,220,128Z"/>',
|
||||
'warning':
|
||||
'<path d="M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z"/>',
|
||||
'alert-triangle':
|
||||
'<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.8a.6.6,0,0,1-.48.2H40.55a.6.6,0,0,1-.48-.2.51.51,0,0,1,0-.52L127.62,51.37a.72.72,0,0,1,1.24,0l87.55,151.91A.51.51,0,0,1,222.93,203.8ZM116,144V104a12,12,0,0,1,24,0v40a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,184Z"/>',
|
||||
'alert-circle':
|
||||
'<path d="M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z"/>',
|
||||
'question':
|
||||
'<path d="M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm0-140a32,32,0,0,0-32,32,12,12,0,0,0,24,0,8,8,0,1,1,8,8,12,12,0,0,0-12,12v8a12,12,0,0,0,24,0v-1.26A32,32,0,0,0,128,72Zm12,100a16,16,0,1,1-16-16A16,16,0,0,1,140,172Z"/>',
|
||||
'house':
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.svelte"]
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@
|
|||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
|
|||
|
|
@ -7,14 +7,39 @@
|
|||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./SubscriptionCard.svelte": "./src/SubscriptionCard.svelte",
|
||||
"./PackageCard.svelte": "./src/PackageCard.svelte",
|
||||
"./BillingToggle.svelte": "./src/BillingToggle.svelte",
|
||||
"./UsageCard.svelte": "./src/UsageCard.svelte",
|
||||
"./CostCard.svelte": "./src/CostCard.svelte",
|
||||
"./SubscriptionButton.svelte": "./src/SubscriptionButton.svelte",
|
||||
"./ManaIcon.svelte": "./src/ManaIcon.svelte"
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./SubscriptionCard.svelte": {
|
||||
"svelte": "./src/SubscriptionCard.svelte",
|
||||
"default": "./src/SubscriptionCard.svelte"
|
||||
},
|
||||
"./PackageCard.svelte": {
|
||||
"svelte": "./src/PackageCard.svelte",
|
||||
"default": "./src/PackageCard.svelte"
|
||||
},
|
||||
"./BillingToggle.svelte": {
|
||||
"svelte": "./src/BillingToggle.svelte",
|
||||
"default": "./src/BillingToggle.svelte"
|
||||
},
|
||||
"./UsageCard.svelte": {
|
||||
"svelte": "./src/UsageCard.svelte",
|
||||
"default": "./src/UsageCard.svelte"
|
||||
},
|
||||
"./CostCard.svelte": {
|
||||
"svelte": "./src/CostCard.svelte",
|
||||
"default": "./src/CostCard.svelte"
|
||||
},
|
||||
"./SubscriptionButton.svelte": {
|
||||
"svelte": "./src/SubscriptionButton.svelte",
|
||||
"default": "./src/SubscriptionButton.svelte"
|
||||
},
|
||||
"./ManaIcon.svelte": {
|
||||
"svelte": "./src/ManaIcon.svelte",
|
||||
"default": "./src/ManaIcon.svelte"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
let isHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative rounded-xl p-4 transition-all duration-200 bg-content border hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class:border-mana={pkg.popular}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
let isHovered = $state(false);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative rounded-xl p-4 transition-all duration-200 bg-content border hover:-translate-y-0.5 hover:shadow-lg"
|
||||
class:border-2={isCurrentPlan}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,24 @@ export function createSupabaseAdminClient(config: SupabaseConfig): SupabaseClien
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Supabase error type
|
||||
*/
|
||||
export interface SupabaseError {
|
||||
message: string;
|
||||
code?: string;
|
||||
details?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized query result type
|
||||
*/
|
||||
export interface QueryResult<T> {
|
||||
data: T | null;
|
||||
error: SupabaseError | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common database query helpers
|
||||
*/
|
||||
|
|
@ -44,16 +62,15 @@ export const dbHelpers = {
|
|||
/**
|
||||
* Handle Supabase query result and return standardized response
|
||||
*/
|
||||
handleQueryResult<T>(result: { data: T | null; error: any }): {
|
||||
data: T | null;
|
||||
error: { message: string; code?: string } | null;
|
||||
} {
|
||||
handleQueryResult<T>(result: { data: T | null; error: SupabaseError | null }): QueryResult<T> {
|
||||
if (result.error) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
message: result.error.message,
|
||||
code: result.error.code,
|
||||
details: result.error.details,
|
||||
hint: result.error.hint,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist"
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
"./colors": "./src/colors.js",
|
||||
"./theme.css": "./src/theme-variables.css",
|
||||
"./themes.css": "./src/themes.css",
|
||||
"./components.css": "./src/components.css"
|
||||
"./components.css": "./src/components.css",
|
||||
"./v4": "./src/tailwind-v4.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^3.0.0 || ^4.0.0"
|
||||
|
|
|
|||
117
packages/shared-tailwind/src/tailwind-v4.css
Normal file
117
packages/shared-tailwind/src/tailwind-v4.css
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Tailwind CSS v4 Configuration
|
||||
*
|
||||
* This file provides the CSS-first configuration for Tailwind v4.
|
||||
* Import this file in your app.css instead of using tailwind.config.js
|
||||
*
|
||||
* Usage:
|
||||
* @import "tailwindcss";
|
||||
* @import "@manacore/shared-tailwind/v4";
|
||||
*/
|
||||
|
||||
/* ===== Theme Configuration ===== */
|
||||
@theme {
|
||||
/* Brand color */
|
||||
--color-mana: #4287f5;
|
||||
|
||||
/* Semantic colors using CSS variables */
|
||||
--color-background: hsl(var(--color-background));
|
||||
--color-foreground: hsl(var(--color-foreground));
|
||||
|
||||
--color-primary: hsl(var(--color-primary));
|
||||
--color-primary-foreground: hsl(var(--color-primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--color-secondary));
|
||||
--color-secondary-foreground: hsl(var(--color-secondary-foreground));
|
||||
|
||||
--color-surface: hsl(var(--color-surface));
|
||||
--color-surface-hover: hsl(var(--color-surface-hover));
|
||||
--color-surface-elevated: hsl(var(--color-surface-elevated));
|
||||
|
||||
--color-muted: hsl(var(--color-muted));
|
||||
--color-muted-foreground: hsl(var(--color-muted-foreground));
|
||||
|
||||
--color-border: hsl(var(--color-border));
|
||||
--color-border-strong: hsl(var(--color-border-strong));
|
||||
|
||||
--color-error: hsl(var(--color-error));
|
||||
--color-success: hsl(var(--color-success));
|
||||
--color-warning: hsl(var(--color-warning));
|
||||
|
||||
--color-input: hsl(var(--color-input));
|
||||
--color-ring: hsl(var(--color-ring));
|
||||
|
||||
/* Legacy aliases */
|
||||
--color-content: hsl(var(--color-surface));
|
||||
--color-content-hover: hsl(var(--color-surface-hover));
|
||||
--color-content-page: hsl(var(--color-background));
|
||||
|
||||
--color-menu: hsl(var(--color-muted));
|
||||
--color-menu-hover: hsl(var(--color-surface-hover));
|
||||
|
||||
--color-theme: hsl(var(--color-foreground));
|
||||
--color-theme-secondary: hsl(var(--color-muted-foreground));
|
||||
|
||||
--color-primary-btn: hsl(var(--color-primary));
|
||||
--color-primary-btn-text: hsl(var(--color-primary-foreground));
|
||||
|
||||
/* Border radius */
|
||||
--radius-none: 0;
|
||||
--radius-sm: var(--radius-sm, 0.25rem);
|
||||
--radius-DEFAULT: var(--radius, 0.375rem);
|
||||
--radius-md: var(--radius-md, 0.5rem);
|
||||
--radius-lg: var(--radius-lg, 0.75rem);
|
||||
--radius-xl: var(--radius-xl, 1rem);
|
||||
--radius-2xl: var(--radius-2xl, 1.5rem);
|
||||
--radius-3xl: var(--radius-3xl, 2rem);
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Box shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-DEFAULT: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-none: none;
|
||||
|
||||
/* Font families */
|
||||
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
/* Transition durations */
|
||||
--duration-250: 250ms;
|
||||
--duration-350: 350ms;
|
||||
--duration-400: 400ms;
|
||||
|
||||
/* Animations */
|
||||
--animate-spin-slow: spin 3s linear infinite;
|
||||
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--animate-bounce-slow: bounce 2s infinite;
|
||||
--animate-fade-in: fadeIn 0.2s ease-out;
|
||||
--animate-fade-out: fadeOut 0.2s ease-in;
|
||||
--animate-slide-in: slideIn 0.2s ease-out;
|
||||
--animate-slide-out: slideOut 0.2s ease-in;
|
||||
}
|
||||
|
||||
/* ===== Keyframes ===== */
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateY(-10px); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
0% { transform: translateY(0); opacity: 1; }
|
||||
100% { transform: translateY(-10px); opacity: 0; }
|
||||
}
|
||||
|
|
@ -5,12 +5,10 @@
|
|||
* Variables are set by @manacore/shared-theme's createThemeStore() at runtime,
|
||||
* but this file provides sensible defaults for static rendering.
|
||||
*
|
||||
* Usage in app.css:
|
||||
* Usage in app.css (Tailwind v4):
|
||||
* ```css
|
||||
* @import '@manacore/shared-tailwind/themes.css';
|
||||
* @tailwind base;
|
||||
* @tailwind components;
|
||||
* @tailwind utilities;
|
||||
* @import "tailwindcss";
|
||||
* @import "@manacore/shared-tailwind/themes.css";
|
||||
* ```
|
||||
*
|
||||
* Color format: HSL values without hsl() wrapper
|
||||
|
|
@ -18,6 +16,105 @@
|
|||
* Used as: hsl(var(--color-primary))
|
||||
*/
|
||||
|
||||
/* ===== Tailwind v4 Theme Configuration ===== */
|
||||
@theme {
|
||||
/* Brand color */
|
||||
--color-mana: #4287f5;
|
||||
|
||||
/* Semantic colors using CSS variables */
|
||||
--color-background: hsl(var(--color-background));
|
||||
--color-foreground: hsl(var(--color-foreground));
|
||||
|
||||
--color-primary: hsl(var(--color-primary));
|
||||
--color-primary-foreground: hsl(var(--color-primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--color-secondary));
|
||||
--color-secondary-foreground: hsl(var(--color-secondary-foreground));
|
||||
|
||||
--color-surface: hsl(var(--color-surface));
|
||||
--color-surface-hover: hsl(var(--color-surface-hover));
|
||||
--color-surface-elevated: hsl(var(--color-surface-elevated));
|
||||
|
||||
--color-muted: hsl(var(--color-muted));
|
||||
--color-muted-foreground: hsl(var(--color-muted-foreground));
|
||||
|
||||
--color-border: hsl(var(--color-border));
|
||||
--color-border-strong: hsl(var(--color-border-strong));
|
||||
|
||||
--color-error: hsl(var(--color-error));
|
||||
--color-success: hsl(var(--color-success));
|
||||
--color-warning: hsl(var(--color-warning));
|
||||
|
||||
--color-input: hsl(var(--color-input));
|
||||
--color-ring: hsl(var(--color-ring));
|
||||
|
||||
/* Legacy aliases */
|
||||
--color-content: hsl(var(--color-surface));
|
||||
--color-content-hover: hsl(var(--color-surface-hover));
|
||||
--color-content-page: hsl(var(--color-background));
|
||||
--color-menu: hsl(var(--color-muted));
|
||||
--color-menu-hover: hsl(var(--color-surface-hover));
|
||||
--color-theme: hsl(var(--color-foreground));
|
||||
--color-theme-secondary: hsl(var(--color-muted-foreground));
|
||||
--color-primary-btn: hsl(var(--color-primary));
|
||||
--color-primary-btn-text: hsl(var(--color-primary-foreground));
|
||||
|
||||
/* Border radius */
|
||||
--radius-none: 0;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-DEFAULT: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-3xl: 2rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Box shadows */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-DEFAULT: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-none: none;
|
||||
|
||||
/* Font families */
|
||||
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
/* Animations */
|
||||
--animate-spin-slow: spin 3s linear infinite;
|
||||
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--animate-bounce-slow: bounce 2s infinite;
|
||||
--animate-fade-in: fadeIn 0.2s ease-out;
|
||||
--animate-fade-out: fadeOut 0.2s ease-in;
|
||||
--animate-slide-in: slideIn 0.2s ease-out;
|
||||
--animate-slide-out: slideOut 0.2s ease-in;
|
||||
}
|
||||
|
||||
/* ===== Keyframes ===== */
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
0% { transform: translateY(-10px); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
0% { transform: translateY(0); opacity: 1; }
|
||||
100% { transform: translateY(-10px); opacity: 0; }
|
||||
}
|
||||
|
||||
/* ===== Default Theme (Lume Light) ===== */
|
||||
:root {
|
||||
/* Primary brand color */
|
||||
|
|
|
|||
|
|
@ -3,13 +3,27 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"svelte": "./src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./ThemeToggle.svelte": "./src/ThemeToggle.svelte",
|
||||
"./ThemeSelector.svelte": "./src/ThemeSelector.svelte",
|
||||
"./ThemeModeSelector.svelte": "./src/ThemeModeSelector.svelte"
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./ThemeToggle.svelte": {
|
||||
"svelte": "./src/ThemeToggle.svelte",
|
||||
"default": "./src/ThemeToggle.svelte"
|
||||
},
|
||||
"./ThemeSelector.svelte": {
|
||||
"svelte": "./src/ThemeSelector.svelte",
|
||||
"default": "./src/ThemeSelector.svelte"
|
||||
},
|
||||
"./ThemeModeSelector.svelte": {
|
||||
"svelte": "./src/ThemeModeSelector.svelte",
|
||||
"default": "./src/ThemeModeSelector.svelte"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,29 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"svelte": "./src/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./store": "./src/store.svelte.ts",
|
||||
"./types": "./src/types.ts",
|
||||
"./constants": "./src/constants.ts",
|
||||
"./utils": "./src/utils.ts"
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./store": {
|
||||
"svelte": "./src/store.svelte.ts",
|
||||
"default": "./src/store.svelte.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
},
|
||||
"./constants": {
|
||||
"default": "./src/constants.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"default": "./src/utils.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020"],
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist"
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,26 @@
|
|||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./atoms": "./src/atoms/index.ts",
|
||||
"./molecules": "./src/molecules/index.ts",
|
||||
"./organisms": "./src/organisms/index.ts"
|
||||
".": {
|
||||
"svelte": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./atoms": {
|
||||
"svelte": "./src/atoms/index.ts",
|
||||
"types": "./src/atoms/index.ts",
|
||||
"default": "./src/atoms/index.ts"
|
||||
},
|
||||
"./molecules": {
|
||||
"svelte": "./src/molecules/index.ts",
|
||||
"types": "./src/molecules/index.ts",
|
||||
"default": "./src/molecules/index.ts"
|
||||
},
|
||||
"./organisms": {
|
||||
"svelte": "./src/organisms/index.ts",
|
||||
"types": "./src/organisms/index.ts",
|
||||
"default": "./src/organisms/index.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
const isInteractive = $derived(interactive || !!onclick);
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
class="card card--{variant} card--padding-{padding} {isInteractive ? 'card--interactive' : ''} {fullWidth ? 'card--full-width' : ''} {className}"
|
||||
{onclick}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type TextVariant = 'body' | 'body-secondary' | 'small' | 'large' | 'muted';
|
||||
type TextAlign = 'left' | 'center' | 'right';
|
||||
|
|
@ -10,7 +11,7 @@
|
|||
align?: TextAlign;
|
||||
weight?: TextWeight;
|
||||
class?: string;
|
||||
children?: any;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@
|
|||
|
||||
<!-- Actions -->
|
||||
{#if actions}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="data-card__actions flex-shrink-0 flex items-center gap-1" onclick={(e) => e.stopPropagation()}>
|
||||
{@render actions()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
required?: boolean;
|
||||
autocomplete?: HTMLInputAttributes['autocomplete'];
|
||||
class?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -26,7 +28,9 @@
|
|||
disabled = false,
|
||||
required = false,
|
||||
autocomplete,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `input-${Math.random().toString(36).slice(2, 9)}`,
|
||||
name
|
||||
}: Props = $props();
|
||||
|
||||
function handleInput(e: Event) {
|
||||
|
|
@ -43,15 +47,17 @@
|
|||
|
||||
<div class="flex flex-col gap-1.5 {className}">
|
||||
{#if label}
|
||||
<label class="text-sm font-medium text-theme">
|
||||
<label for={id} class="text-sm font-medium text-theme">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-500">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{type}
|
||||
{value}
|
||||
{placeholder}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
<script lang="ts">
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
import type { SelectOption } from './Select.types';
|
||||
|
||||
interface Props {
|
||||
/** Current selected value */
|
||||
|
|
@ -24,6 +20,8 @@
|
|||
required?: boolean;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
/** Unique ID for accessibility */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -35,7 +33,8 @@
|
|||
error,
|
||||
disabled = false,
|
||||
required = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `select-${Math.random().toString(36).slice(2, 9)}`
|
||||
}: Props = $props();
|
||||
|
||||
function handleChange(e: Event) {
|
||||
|
|
@ -47,7 +46,7 @@
|
|||
|
||||
<div class="select-wrapper {className}">
|
||||
{#if label}
|
||||
<label class="select-label">
|
||||
<label for={id} class="select-label">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="select-required">*</span>
|
||||
|
|
@ -57,6 +56,7 @@
|
|||
|
||||
<div class="select-container">
|
||||
<select
|
||||
{id}
|
||||
{value}
|
||||
{disabled}
|
||||
{required}
|
||||
|
|
|
|||
5
packages/shared-ui/src/molecules/Select.types.ts
Normal file
5
packages/shared-ui/src/molecules/Select.types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@
|
|||
autoResize?: boolean;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
/** Unique ID for accessibility */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -41,7 +43,8 @@
|
|||
disabled = false,
|
||||
required = false,
|
||||
autoResize = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
id = `textarea-${Math.random().toString(36).slice(2, 9)}`
|
||||
}: Props = $props();
|
||||
|
||||
let textareaElement: HTMLTextAreaElement | null = $state(null);
|
||||
|
|
@ -68,7 +71,7 @@
|
|||
|
||||
<div class="textarea-wrapper {className}">
|
||||
{#if label}
|
||||
<label class="textarea-label">
|
||||
<label for={id} class="textarea-label">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="textarea-required">*</span>
|
||||
|
|
@ -77,6 +80,7 @@
|
|||
{/if}
|
||||
|
||||
<textarea
|
||||
{id}
|
||||
bind:this={textareaElement}
|
||||
{value}
|
||||
{placeholder}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
: 'bg-menu'} {disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}"
|
||||
role="switch"
|
||||
aria-checked={isOn}
|
||||
aria-label="Toggle"
|
||||
{disabled}
|
||||
>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
onclick={handleClick}
|
||||
onkeydown={handleKeyDown}
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabindex={clickable ? 0 : -1}
|
||||
tabindex={clickable ? 0 : undefined}
|
||||
>
|
||||
<!-- Color indicator dot -->
|
||||
<div class="h-2 w-2 rounded-full" style="background-color: {tagColor}"></div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export interface AppItem {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
import type { AppItem } from './AppSlider.types';
|
||||
|
||||
interface Props {
|
||||
apps: AppItem[];
|
||||
|
|
@ -120,17 +110,10 @@
|
|||
<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;">
|
||||
{#each apps as app, index}
|
||||
<button
|
||||
class="group relative flex-shrink-0 rounded-2xl p-5 cursor-pointer snap-center"
|
||||
style="width: 160px; background-color: {hoveredApp === index
|
||||
? isDark
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'rgba(0, 0, 0, 0.08)'
|
||||
: isDark
|
||||
? 'rgba(255, 255, 255, 0.05)'
|
||||
: 'rgba(0, 0, 0, 0.05)'}; border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; box-shadow: 0 4px 20px rgba(0, 0, 0, {isDark ? '0.3' : '0.15'}); 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="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)'};"
|
||||
onmouseenter={() => hoveredApp = index}
|
||||
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
|
||||
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
|
||||
onmouseleave={() => hoveredApp = null}
|
||||
onclick={() => openModal(index)}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
9
packages/shared-ui/src/organisms/AppSlider.types.ts
Normal file
9
packages/shared-ui/src/organisms/AppSlider.types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export interface AppItem {
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
icon?: string;
|
||||
color: string;
|
||||
comingSoon?: boolean;
|
||||
status: 'published' | 'beta' | 'development' | 'planning';
|
||||
}
|
||||
|
|
@ -42,16 +42,21 @@
|
|||
|
||||
{#if visible}
|
||||
<!-- Modal Backdrop -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleBackdropClick(e as unknown as MouseEvent)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="relative flex max-h-[90vh] w-full {maxWidthClasses[maxWidth]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{#if showHeader}
|
||||
<!-- Header -->
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"types": ["svelte"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist"
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue