🔐 style: update auth and loading screen (#3875)

* style: improve auth UI

* style(SocialButton): fix hover style

* remove testing files

* fix: package-lock

* feat: loading screen color based on theme

* fix: handle `system` style on loading screen

* fix(ThemeSelector): Correct icon and text color handling for `system` theme

* remove test file
This commit is contained in:
Marco Beretta 2024-09-11 04:20:19 -09:00 committed by GitHub
parent 020995514e
commit 35a89bfa99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 110 deletions

View file

@ -2,47 +2,57 @@
<html lang="en-US"> <html lang="en-US">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="theme-color" content="#171717"> <meta name="theme-color" content="#171717" />
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>LibreChat</title> <title>LibreChat</title>
<link <link rel="shortcut icon" href="#" />
rel="shortcut icon" <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
href="#" <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
/> <link rel="apple-touch-icon" href="/assets/apple-touch-icon-180x180.png" />
<link <meta name="viewport" content="width=device-width, initial-scale=1" />
rel="icon" <style>
type="image/png" html,
sizes="32x32" body {
href="/assets/favicon-32x32.png" margin: 0;
/> padding: 0;
<link height: 100%;
rel="icon" }
type="image/png" </style>
sizes="16x16" <script>
href="/assets/favicon-16x16.png" const theme = localStorage.getItem('color-theme');
/> const loadingContainerStyle = document.createElement('style');
<link let backgroundColor;
rel="apple-touch-icon"
href="/assets/apple-touch-icon-180x180.png" if (theme === 'dark') {
/> backgroundColor = '#0d0d0d';
<meta } else if (theme === 'light') {
name="viewport" backgroundColor = '#ffffff';
content="width=device-width, initial-scale=1" } else if (theme === 'system') {
/> const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
<script backgroundColor = prefersDarkScheme ? '#0d0d0d' : '#ffffff';
defer } else {
type="module" backgroundColor = '#ffffff';
src="/src/main.jsx" }
></script>
loadingContainerStyle.innerHTML = `
#loading-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background-color: ${backgroundColor};
}
`;
document.head.appendChild(loadingContainerStyle);
</script>
<script defer type="module" src="/src/main.jsx"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root">
<div id="loading-container"></div>
<script </div>
type="module" <script type="module" src="/src/main.jsx"></script>
src="/src/main.jsx"
></script>
</body> </body>
</html> </html>

View file

@ -57,7 +57,7 @@ function AuthLayout({
return ( return (
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900"> <div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<BlinkAnimation active={isFetching}> <BlinkAnimation active={isFetching}>
<div className="mt-12 h-24 w-full bg-cover"> <div className="mt-6 h-10 w-full bg-cover">
<img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" /> <img src="/assets/logo.svg" className="h-full w-full object-contain" alt="Logo" />
</div> </div>
</BlinkAnimation> </BlinkAnimation>

View file

@ -81,7 +81,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
method="POST" method="POST"
onSubmit={handleSubmit((data) => onSubmit(data))} onSubmit={handleSubmit((data) => onSubmit(data))}
> >
<div className="mb-2"> <div className="mb-4">
<div className="relative"> <div className="relative">
<input <input
type="text" type="text"
@ -97,12 +97,20 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
}, },
})} })}
aria-invalid={!!errors.email} aria-invalid={!!errors.email}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="email" htmlFor="email"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{useUsernameLogin {useUsernameLogin
? localize('com_auth_username').replace(/ \(.*$/, '') ? localize('com_auth_username').replace(/ \(.*$/, '')
@ -124,12 +132,20 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
maxLength: { value: 128, message: localize('com_auth_password_max_length') }, maxLength: { value: 128, message: localize('com_auth_password_max_length') },
})} })}
aria-invalid={!!errors.password} aria-invalid={!!errors.password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="password" htmlFor="password"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-600 dark:peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{localize('com_auth_password')} {localize('com_auth_password')}
</label> </label>
@ -146,7 +162,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
aria-label="Sign in" aria-label="Sign in"
data-testid="login-button" data-testid="login-button"
type="submit" type="submit"
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500" className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
> >
{localize('com_auth_continue')} {localize('com_auth_continue')}
</button> </button>

View file

@ -57,7 +57,7 @@ const Registration: React.FC = () => {
}); });
const renderInput = (id: string, label: string, type: string, validation: object) => ( const renderInput = (id: string, label: string, type: string, validation: object) => (
<div className="mb-2"> <div className="mb-4">
<div className="relative"> <div className="relative">
<input <input
id={id} id={id}
@ -69,19 +69,27 @@ const Registration: React.FC = () => {
validation, validation,
)} )}
aria-invalid={!!errors[id]} aria-invalid={!!errors[id]}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
data-testid={id} data-testid={id}
/> />
<label <label
htmlFor={id} htmlFor={id}
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{localize(label)} {localize(label)}
</label> </label>
</div> </div>
{errors[id] && ( {errors[id] && (
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900"> <span role="alert" className="mt-1 text-sm text-red-500">
{String(errors[id]?.message) ?? ''} {String(errors[id]?.message) ?? ''}
</span> </span>
)} )}
@ -175,7 +183,7 @@ const Registration: React.FC = () => {
disabled={Object.keys(errors).length > 0} disabled={Object.keys(errors).length > 0}
type="submit" type="submit"
aria-label="Submit registration" aria-label="Submit registration"
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500" className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
> >
{isSubmitting ? <Spinner /> : localize('com_auth_continue')} {isSubmitting ? <Spinner /> : localize('com_auth_continue')}
</button> </button>

View file

@ -104,12 +104,20 @@ function RequestPasswordReset() {
}, },
})} })}
aria-invalid={!!errors.email} aria-invalid={!!errors.email}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="email" htmlFor="email"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{localize('com_auth_email_address')} {localize('com_auth_email_address')}
</label> </label>
@ -124,7 +132,7 @@ function RequestPasswordReset() {
<button <button
type="submit" type="submit"
disabled={!!errors.email} disabled={!!errors.email}
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-550 focus:bg-green-550 focus:outline-none disabled:cursor-not-allowed disabled:hover:bg-green-500" className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
> >
{localize('com_auth_continue')} {localize('com_auth_continue')}
</button> </button>

View file

@ -89,12 +89,20 @@ function ResetPassword() {
}, },
})} })}
aria-invalid={!!errors.password} aria-invalid={!!errors.password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="password" htmlFor="password"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{localize('com_auth_password')} {localize('com_auth_password')}
</label> </label>
@ -116,12 +124,20 @@ function ResetPassword() {
validate: (value) => value === password || localize('com_auth_password_not_match'), validate: (value) => value === password || localize('com_auth_password_not_match'),
})} })}
aria-invalid={!!errors.confirm_password} aria-invalid={!!errors.confirm_password}
className="webkit-dark-styles peer block w-full appearance-none rounded-md border border-gray-300 bg-transparent px-3.5 pb-3.5 pt-4 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500" className="
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
bg-surface-primary px-3.5 pb-2.5 pt-3 duration-200 focus:border-green-500 focus:outline-none
"
placeholder=" " placeholder=" "
/> />
<label <label
htmlFor="confirm_password" htmlFor="confirm_password"
className="absolute start-1 top-2 z-10 origin-[0] -translate-y-4 scale-75 transform bg-white px-3 text-sm text-gray-500 duration-100 peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100 peer-focus:top-2 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-3 peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400 dark:peer-focus:text-green-500 rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4" className="
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
"
> >
{localize('com_auth_password_confirm')} {localize('com_auth_password_confirm')}
</label> </label>
@ -147,7 +163,7 @@ function ResetPassword() {
disabled={!!errors.password || !!errors.confirm_password} disabled={!!errors.password || !!errors.confirm_password}
type="submit" type="submit"
aria-label={localize('com_auth_submit_registration')} aria-label={localize('com_auth_submit_registration')}
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-all duration-300 hover:bg-green-550 focus:bg-green-550 focus:outline-none" className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
> >
{localize('com_auth_continue')} {localize('com_auth_continue')}
</button> </button>

View file

@ -1,60 +1,17 @@
import React, { useState } from 'react'; import React from 'react';
const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) => { const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) => {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [activeButton, setActiveButton] = useState(null);
if (!enabled) { if (!enabled) {
return null; return null;
} }
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
if (isPressed) {
setIsPressed(false);
}
};
const handleMouseDown = () => {
setIsPressed(true);
setActiveButton(id);
};
const handleMouseUp = () => {
setIsPressed(false);
};
const getButtonStyles = () => {
// Define Tailwind CSS classes based on state
const baseStyles = 'border border-solid border-gray-300 dark:border-gray-600 transition-colors';
let dynamicStyles = '';
if (isPressed && activeButton === id) {
dynamicStyles = 'bg-blue-200 border-blue-200 dark:bg-blue-900 dark:border-blue-600';
} else if (isHovered) {
dynamicStyles = 'bg-gray-100 dark:bg-gray-700';
}
return `${baseStyles} ${dynamicStyles}`;
};
return ( return (
<div className="mt-2 flex gap-x-2"> <div className="mt-2 flex gap-x-2">
<a <a
aria-label={`${label}`} aria-label={`${label}`}
className={`${getButtonStyles()} flex w-full items-center space-x-3 rounded-md px-5 py-3 text-black transition-colors dark:text-white`} className="flex w-full items-center space-x-3 rounded-2xl border border-border-light bg-surface-primary px-5 py-3 text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
href={`${serverDomain}/oauth/${oauthPath}`} href={`${serverDomain}/oauth/${oauthPath}`}
data-testid={id} data-testid={id}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
> >
<Icon /> <Icon />
<p>{label}</p> <p>{label}</p>

View file

@ -1,25 +1,37 @@
import React, { useContext, useCallback } from 'react'; import React, { useContext, useCallback, useEffect } from 'react';
import { Sun, Moon } from 'lucide-react'; import { Sun, Moon, Monitor } from 'lucide-react';
import { ThemeContext } from '~/hooks'; import { ThemeContext } from '~/hooks';
const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => { const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => {
const themeIcons = { const themeIcons = {
system: <Sun />, system: <Monitor />,
dark: <Moon color="white" />, dark: <Moon color="white" />,
light: <Sun />, light: <Sun />,
}; };
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="cursor-pointer" onClick={() => onChange(theme === 'dark' ? 'light' : 'dark')}> <button
className="cursor-pointer"
onClick={() => onChange(theme === 'dark' ? 'light' : 'dark')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onChange(theme === 'dark' ? 'light' : 'dark');
}
}}
role="switch"
aria-checked={theme === 'dark'}
tabIndex={0}
>
{themeIcons[theme]} {themeIcons[theme]}
</div> </button>
</div> </div>
); );
}; };
const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => { const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
const { theme, setTheme } = useContext(ThemeContext); const { theme, setTheme } = useContext(ThemeContext);
const changeTheme = useCallback( const changeTheme = useCallback(
(value: string) => { (value: string) => {
setTheme(value); setTheme(value);
@ -27,7 +39,14 @@ const ThemeSelector = ({ returnThemeOnly }: { returnThemeOnly?: boolean }) => {
[setTheme], [setTheme],
); );
if (returnThemeOnly) { useEffect(() => {
if (theme === 'system') {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDarkScheme ? 'dark' : 'light');
}
}, [theme, setTheme]);
if (returnThemeOnly === true) {
return <Theme theme={theme} onChange={changeTheme} />; return <Theme theme={theme} onChange={changeTheme} />;
} }