mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 22:58:18 +00:00
UI improvements
This commit is contained in:
@@ -7,15 +7,14 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
UnstyledButton,
|
|
||||||
rem,
|
rem,
|
||||||
useComputedColorScheme,
|
useComputedColorScheme,
|
||||||
useMantineColorScheme,
|
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
type BoxProps,
|
type BoxProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
|
import { IconCheck } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ColorSchemeToggle, LanguageSwitcher } from '@ema-platform/ui';
|
||||||
import { useAuthConfig } from '../AuthConfig';
|
import { useAuthConfig } from '../AuthConfig';
|
||||||
|
|
||||||
export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number }) {
|
export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number }) {
|
||||||
@@ -33,35 +32,6 @@ export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ThemeToggle() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { setColorScheme } = useMantineColorScheme();
|
|
||||||
const computed = useComputedColorScheme('light');
|
|
||||||
const isDark = computed === 'dark';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UnstyledButton
|
|
||||||
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
|
|
||||||
aria-label={t('authShell.toggleTheme', 'Toggle theme')}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: rem(36),
|
|
||||||
height: rem(36),
|
|
||||||
borderRadius: rem(10),
|
|
||||||
border: '1px solid var(--mantine-color-default-border)',
|
|
||||||
background: 'var(--mantine-color-body)',
|
|
||||||
color: 'var(--mantine-color-dimmed)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 150ms ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDark ? <IconSun size={18} /> : <IconMoon size={18} />}
|
|
||||||
</UnstyledButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthShellProps {
|
interface AuthShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
brandTitle?: string;
|
brandTitle?: string;
|
||||||
@@ -98,8 +68,9 @@ export function AuthShell({ children, brandTitle, brandSubtitle }: AuthShellProp
|
|||||||
}}
|
}}
|
||||||
p="md"
|
p="md"
|
||||||
>
|
>
|
||||||
{/* Theme toggle — top-right corner */}
|
{/* Language switcher & Theme toggle — top-right corner */}
|
||||||
<Box
|
<Group
|
||||||
|
gap="xs"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: rem(16),
|
top: rem(16),
|
||||||
@@ -107,8 +78,9 @@ export function AuthShell({ children, brandTitle, brandSubtitle }: AuthShellProp
|
|||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThemeToggle />
|
<LanguageSwitcher supportedLanguages={['en', 'am']} />
|
||||||
</Box>
|
<ColorSchemeToggle />
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
mih="100vh"
|
mih="100vh"
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconDeviceMobile,
|
IconDeviceMobile,
|
||||||
IconLock,
|
IconLock,
|
||||||
@@ -50,6 +52,14 @@ export function LoginPage() {
|
|||||||
const [meTrigger] = useApiMutation<AuthUser>();
|
const [meTrigger] = useApiMutation<AuthUser>();
|
||||||
const [profileTrigger] = useApiMutation<{ profile: CurrentProfile | null }>();
|
const [profileTrigger] = useApiMutation<{ profile: CurrentProfile | null }>();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Built inside the component (not module scope) so validation messages
|
// Built inside the component (not module scope) so validation messages
|
||||||
// pick up the active language — same pattern as ProfilePage's forms.
|
// pick up the active language — same pattern as ProfilePage's forms.
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -149,6 +159,32 @@ export function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<AuthShell>
|
<AuthShell>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={handleBack}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--mantine-color-dimmed)",
|
||||||
|
cursor: "pointer",
|
||||||
|
width: "fit-content",
|
||||||
|
transition: "all 150ms ease",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.color = "var(--mantine-primary-color-filled)";
|
||||||
|
e.currentTarget.style.transform = "translateX(-3px)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.color = "var(--mantine-color-dimmed)";
|
||||||
|
e.currentTarget.style.transform = "translateX(0)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowLeft size={18} />
|
||||||
|
{t("common.back", "Back")}
|
||||||
|
</UnstyledButton>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
{t("login.welcome", { appName, defaultValue: "Welcome to {{appName}}" })}
|
{t("login.welcome", { appName, defaultValue: "Welcome to {{appName}}" })}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconAt,
|
IconAt,
|
||||||
IconDeviceMobile,
|
IconDeviceMobile,
|
||||||
@@ -61,6 +63,14 @@ export function SignupPage() {
|
|||||||
}>();
|
}>();
|
||||||
const [meTrigger] = useApiMutation<AuthUser>();
|
const [meTrigger] = useApiMutation<AuthUser>();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
navigate(-1);
|
||||||
|
} else {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Order matches passwordRules' default list: length, lowercase, uppercase,
|
// Order matches passwordRules' default list: length, lowercase, uppercase,
|
||||||
// number, special character. Shared between the zod schema (field error)
|
// number, special character. Shared between the zod schema (field error)
|
||||||
// and the live checklist below, so both agree on the wording.
|
// and the live checklist below, so both agree on the wording.
|
||||||
@@ -155,6 +165,32 @@ export function SignupPage() {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
<UnstyledButton
|
||||||
|
onClick={handleBack}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--mantine-color-dimmed)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: 'fit-content',
|
||||||
|
transition: 'all 150ms ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.color = 'var(--mantine-primary-color-filled)';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-3px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.color = 'var(--mantine-color-dimmed)';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowLeft size={18} />
|
||||||
|
{t('common.back', 'Back')}
|
||||||
|
</UnstyledButton>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
{t('signup.title', 'Create account')}
|
{t('signup.title', 'Create account')}
|
||||||
|
|||||||
@@ -1,45 +1,118 @@
|
|||||||
import { UnstyledButton, useMantineColorScheme, useComputedColorScheme, rem } from '@mantine/core';
|
import {
|
||||||
import { IconSun, IconMoon } from '@tabler/icons-react';
|
Menu,
|
||||||
|
UnstyledButton,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
rem,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconSun,
|
||||||
|
IconMoon,
|
||||||
|
IconDeviceDesktop,
|
||||||
|
IconCheck,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function ColorSchemeToggle() {
|
export function ColorSchemeToggle() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setColorScheme } = useMantineColorScheme();
|
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||||
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true });
|
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true });
|
||||||
const isDark = computed === 'dark';
|
const isDark = computed === 'dark';
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
{
|
||||||
|
value: 'light' as const,
|
||||||
|
label: t('profile.appearance.light', 'Light'),
|
||||||
|
icon: IconSun,
|
||||||
|
color: '#e67e22',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark' as const,
|
||||||
|
label: t('profile.appearance.dark', 'Dark'),
|
||||||
|
icon: IconMoon,
|
||||||
|
color: '#9b59b6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'auto' as const,
|
||||||
|
label: t('profile.appearance.system', 'System'),
|
||||||
|
icon: IconDeviceDesktop,
|
||||||
|
color: '#3498db',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CurrentIcon = colorScheme === 'auto' ? IconDeviceDesktop : isDark ? IconMoon : IconSun;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<Menu shadow="lg" width={170} position="bottom-end" withinPortal transitionProps={{ transition: 'pop-top-right', duration: 150 }}>
|
||||||
aria-label={t('common.toggleTheme')}
|
<Menu.Target>
|
||||||
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
|
<UnstyledButton
|
||||||
style={{
|
aria-label={t('common.toggleTheme', 'Toggle light / dark mode')}
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
width: rem(38),
|
justifyContent: 'center',
|
||||||
height: rem(38),
|
width: rem(38),
|
||||||
borderRadius: rem(12),
|
height: rem(38),
|
||||||
background: 'var(--mantine-color-body)',
|
borderRadius: rem(12),
|
||||||
color: 'var(--mantine-color-gray-6)',
|
background: 'var(--mantine-color-body)',
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
|
color: isDark ? 'var(--mantine-color-yellow-4)' : 'var(--mantine-color-blue-6)',
|
||||||
transition: 'all 150ms ease',
|
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)',
|
||||||
cursor: 'pointer',
|
transition: 'all 200ms ease',
|
||||||
border: 'none',
|
cursor: 'pointer',
|
||||||
}}
|
border: 'none',
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
|
'0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px var(--mantine-primary-color-3)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
|
e.currentTarget.style.boxShadow =
|
||||||
e.currentTarget.style.boxShadow =
|
'0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)';
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<CurrentIcon size={19} stroke={1.8} />
|
||||||
{isDark ? <IconSun size={19} /> : <IconMoon size={19} />}
|
</UnstyledButton>
|
||||||
</UnstyledButton>
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown
|
||||||
|
style={{
|
||||||
|
borderRadius: rem(14),
|
||||||
|
padding: rem(6),
|
||||||
|
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Label style={{ fontWeight: 600, fontSize: rem(11), letterSpacing: '0.5px', textTransform: 'uppercase' }}>
|
||||||
|
{t('profile.appearance.title', 'Appearance')}
|
||||||
|
</Menu.Label>
|
||||||
|
{themes.map((theme) => {
|
||||||
|
const Icon = theme.icon;
|
||||||
|
const isSelected = colorScheme === theme.value;
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={theme.value}
|
||||||
|
onClick={() => setColorScheme(theme.value)}
|
||||||
|
leftSection={<Icon size={16} stroke={1.8} style={{ color: isSelected ? 'var(--mantine-primary-color-filled)' : 'currentColor' }} />}
|
||||||
|
rightSection={
|
||||||
|
isSelected ? <IconCheck size={16} stroke={2.4} style={{ color: 'var(--mantine-primary-color-filled)' }} /> : undefined
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderRadius: rem(8),
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
backgroundColor: isSelected ? 'var(--mantine-primary-color-light)' : undefined,
|
||||||
|
color: isSelected ? 'var(--mantine-primary-color-filled)' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm">{theme.label}</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +1,179 @@
|
|||||||
import { Menu, UnstyledButton, Text, rem } from '@mantine/core';
|
import type { ComponentType, SVGProps } from 'react';
|
||||||
import { IconWorld, IconCheck, IconChevronDown } from '@tabler/icons-react';
|
import { Menu, UnstyledButton, Text, Group, rem } from '@mantine/core';
|
||||||
|
import { IconCheck, IconChevronDown, IconWorld } from '@tabler/icons-react';
|
||||||
|
import { GB, ET } from 'country-flag-icons/react/3x2';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface LanguageSwitcherProps {
|
interface LanguageSwitcherProps {
|
||||||
supportedLanguages: readonly string[];
|
supportedLanguages: readonly string[];
|
||||||
/** `icon` renders a compact globe button; `button` shows the language label. */
|
/** `icon` renders a compact flag button; `button` shows the language label. */
|
||||||
variant?: 'icon' | 'button';
|
variant?: 'icon' | 'button';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LanguageSwitcher({ supportedLanguages, variant = 'icon' }: LanguageSwitcherProps) {
|
type FlagComponentType = ComponentType<SVGProps<SVGSVGElement>>;
|
||||||
|
|
||||||
|
const LANGUAGE_CONFIG: Record<
|
||||||
|
string,
|
||||||
|
{ Flag: FlagComponentType; nativeName: string; shortCode: string }
|
||||||
|
> = {
|
||||||
|
en: { Flag: GB, nativeName: 'English', shortCode: 'EN' },
|
||||||
|
am: { Flag: ET, nativeName: 'አማርኛ', shortCode: 'AM' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LanguageSwitcher({
|
||||||
|
supportedLanguages,
|
||||||
|
variant = 'icon',
|
||||||
|
}: LanguageSwitcherProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const current = i18n.language;
|
const current = i18n.language || 'en';
|
||||||
|
|
||||||
const change = (lng: string) => {
|
const change = (lng: string) => {
|
||||||
if (lng !== current) i18n.changeLanguage(lng);
|
if (lng !== current) i18n.changeLanguage(lng);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentConfig = LANGUAGE_CONFIG[current] || {
|
||||||
|
Flag: null,
|
||||||
|
nativeName: current.toUpperCase(),
|
||||||
|
shortCode: current.toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentFlag = currentConfig.Flag;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={160} position="bottom-end" withinPortal>
|
<Menu
|
||||||
|
shadow="lg"
|
||||||
|
width={190}
|
||||||
|
position="bottom-end"
|
||||||
|
withinPortal
|
||||||
|
transitionProps={{ transition: 'pop-top-right', duration: 150 }}
|
||||||
|
>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
aria-label={t('language.label')}
|
aria-label={t('language.label', 'Language')}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: rem(4),
|
gap: rem(6),
|
||||||
width: variant === 'icon' ? rem(38) : 'auto',
|
width: variant === 'icon' ? rem(38) : 'auto',
|
||||||
height: rem(38),
|
height: rem(38),
|
||||||
padding: variant === 'icon' ? 0 : '0 12px',
|
padding: variant === 'icon' ? 0 : '0 12px',
|
||||||
borderRadius: rem(12),
|
borderRadius: rem(12),
|
||||||
background: 'var(--mantine-color-body)',
|
background: 'var(--mantine-color-body)',
|
||||||
color: 'var(--mantine-color-gray-6)',
|
color: 'var(--mantine-color-gray-7)',
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)',
|
boxShadow: '0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)',
|
||||||
transition: 'all 150ms ease',
|
transition: 'all 200ms ease',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||||
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
|
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
|
'0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px var(--mantine-primary-color-3)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
|
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.06)';
|
'0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.08)';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconWorld size={19} />
|
{CurrentFlag ? (
|
||||||
|
<CurrentFlag
|
||||||
|
style={{
|
||||||
|
width: rem(20),
|
||||||
|
height: rem(14),
|
||||||
|
borderRadius: rem(2),
|
||||||
|
display: 'block',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconWorld size={19} />
|
||||||
|
)}
|
||||||
|
|
||||||
{variant !== 'icon' && (
|
{variant !== 'icon' && (
|
||||||
<>
|
<Group gap={6} wrap="nowrap">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={600}>
|
||||||
{t(`language.${current}`)}
|
{t(`language.${current}`, currentConfig.nativeName)}
|
||||||
</Text>
|
</Text>
|
||||||
<IconChevronDown size={14} />
|
<IconChevronDown size={14} stroke={2} style={{ opacity: 0.7 }} />
|
||||||
</>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown
|
<Menu.Dropdown
|
||||||
style={{ borderRadius: rem(12), padding: rem(6) }}
|
style={{
|
||||||
|
borderRadius: rem(14),
|
||||||
|
padding: rem(6),
|
||||||
|
boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Menu.Label>{t('language.label')}</Menu.Label>
|
<Menu.Label
|
||||||
{supportedLanguages.map((lng) => (
|
style={{
|
||||||
<Menu.Item
|
fontWeight: 600,
|
||||||
key={lng}
|
fontSize: rem(11),
|
||||||
onClick={() => change(lng)}
|
letterSpacing: '0.5px',
|
||||||
rightSection={
|
textTransform: 'uppercase',
|
||||||
current === lng ? <IconCheck size={16} /> : undefined
|
}}
|
||||||
}
|
>
|
||||||
style={{ borderRadius: rem(8) }}
|
{t('language.label', 'Language')}
|
||||||
>
|
</Menu.Label>
|
||||||
{t(`language.${lng}`)}
|
{supportedLanguages.map((lng) => {
|
||||||
</Menu.Item>
|
const config = LANGUAGE_CONFIG[lng] || {
|
||||||
))}
|
Flag: null,
|
||||||
|
nativeName: lng.toUpperCase(),
|
||||||
|
shortCode: lng.toUpperCase(),
|
||||||
|
};
|
||||||
|
const Flag = config.Flag;
|
||||||
|
const isSelected = current === lng;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
key={lng}
|
||||||
|
onClick={() => change(lng)}
|
||||||
|
leftSection={
|
||||||
|
Flag ? (
|
||||||
|
<Flag
|
||||||
|
style={{
|
||||||
|
width: rem(20),
|
||||||
|
height: rem(14),
|
||||||
|
borderRadius: rem(2),
|
||||||
|
display: 'block',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.15)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconWorld size={16} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rightSection={
|
||||||
|
isSelected ? (
|
||||||
|
<IconCheck
|
||||||
|
size={16}
|
||||||
|
stroke={2.4}
|
||||||
|
style={{ color: 'var(--mantine-primary-color-filled)' }}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderRadius: rem(8),
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? 'var(--mantine-primary-color-light)'
|
||||||
|
: undefined,
|
||||||
|
color: isSelected ? 'var(--mantine-primary-color-filled)' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between" wrap="nowrap" style={{ width: '100%' }}>
|
||||||
|
<Text size="sm">{t(`language.${lng}`, config.nativeName)}</Text>
|
||||||
|
</Group>
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user