mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-22 19:46:39 +00:00
Merge branch 'V2' into V2-allow-viewer
This commit is contained in:
commit
000131f7ca
@ -104,6 +104,7 @@
|
|||||||
"green": "Green",
|
"green": "Green",
|
||||||
"blue": "Blue",
|
"blue": "Blue",
|
||||||
"custom": "Custom...",
|
"custom": "Custom...",
|
||||||
|
"comingSoon": "Coming soon",
|
||||||
"WorkInProgess": "Work in progress, May not work or be buggy, Please report any problems!",
|
"WorkInProgess": "Work in progress, May not work or be buggy, Please report any problems!",
|
||||||
"poweredBy": "Powered by",
|
"poweredBy": "Powered by",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
@ -1,24 +1,161 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Menu, Button, ScrollArea, ActionIcon } from '@mantine/core';
|
import { Menu, Button, ScrollArea, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from '../../i18n';
|
import { supportedLanguages } from '../../i18n';
|
||||||
import LocalIcon from './LocalIcon';
|
import LocalIcon from './LocalIcon';
|
||||||
import styles from './LanguageSelector.module.css';
|
import styles from './LanguageSelector.module.css';
|
||||||
|
|
||||||
|
// Types
|
||||||
interface LanguageSelectorProps {
|
interface LanguageSelectorProps {
|
||||||
position?: React.ComponentProps<typeof Menu>['position'];
|
position?: React.ComponentProps<typeof Menu>['position'];
|
||||||
offset?: number;
|
offset?: number;
|
||||||
compact?: boolean; // icon-only trigger
|
compact?: boolean; // icon-only trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = false }: LanguageSelectorProps) => {
|
interface LanguageOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RippleEffect {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
key: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub-components
|
||||||
|
interface LanguageItemProps {
|
||||||
|
option: LanguageOption;
|
||||||
|
index: number;
|
||||||
|
animationTriggered: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: (event: React.MouseEvent) => void;
|
||||||
|
rippleEffect?: RippleEffect | null;
|
||||||
|
pendingLanguage: string | null;
|
||||||
|
compact: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageItem: React.FC<LanguageItemProps> = ({
|
||||||
|
option,
|
||||||
|
index,
|
||||||
|
animationTriggered,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
rippleEffect,
|
||||||
|
pendingLanguage,
|
||||||
|
compact,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const label = disabled ? (
|
||||||
|
<Tooltip label={t('comingSoon', 'Coming soon')} position="left" withArrow>
|
||||||
|
<p>{option.label}</p>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<p>{option.label}</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.languageItem}
|
||||||
|
style={{
|
||||||
|
opacity: animationTriggered ? 1 : 0,
|
||||||
|
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
||||||
|
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
onClick={disabled ? undefined : onClick}
|
||||||
|
data-selected={isSelected}
|
||||||
|
disabled={disabled}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: '4px',
|
||||||
|
minHeight: '32px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
||||||
|
: 'transparent',
|
||||||
|
color: disabled
|
||||||
|
? 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))'
|
||||||
|
: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
||||||
|
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
||||||
|
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
'&:hover': !disabled ? {
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
||||||
|
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
} : {}
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: isSelected ? 600 : 400,
|
||||||
|
textAlign: 'left',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 2,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{!compact && rippleEffect && pendingLanguage === option.value && (
|
||||||
|
<div
|
||||||
|
key={rippleEffect.key}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: rippleEffect.x,
|
||||||
|
top: rippleEffect.y,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: 'var(--mantine-color-blue-4)',
|
||||||
|
opacity: 0.6,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RippleStyles: React.FC = () => (
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes ripple-expand {
|
||||||
|
0% { width: 0; height: 0; opacity: 0.6; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
100% { width: 100px; height: 100px; opacity: 0; }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main component
|
||||||
|
const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-start', offset = 8, compact = false }) => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||||
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
||||||
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
const [rippleEffect, setRippleEffect] = useState<RippleEffect | null>(null);
|
||||||
|
|
||||||
const languageOptions = Object.entries(supportedLanguages)
|
const languageOptions: LanguageOption[] = Object.entries(supportedLanguages)
|
||||||
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
||||||
.map(([code, name]) => ({
|
.map(([code, name]) => ({
|
||||||
value: code,
|
value: code,
|
||||||
@ -65,15 +202,7 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>
|
<RippleStyles />
|
||||||
{`
|
|
||||||
@keyframes ripple-expand {
|
|
||||||
0% { width: 0; height: 0; opacity: 0.6; }
|
|
||||||
50% { opacity: 0.3; }
|
|
||||||
100% { width: 100px; height: 100px; opacity: 0; }
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Menu
|
<Menu
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onChange={setOpened}
|
onChange={setOpened}
|
||||||
@ -139,79 +268,25 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
>
|
>
|
||||||
<ScrollArea h={190} type="scroll">
|
<ScrollArea h={190} type="scroll">
|
||||||
<div className={styles.languageGrid}>
|
<div className={styles.languageGrid}>
|
||||||
{languageOptions.map((option, index) => (
|
{languageOptions.map((option, index) => {
|
||||||
<div
|
const isEnglishGB = option.value === 'en-GB'; // Currently only English GB has enough translations to use
|
||||||
|
const isDisabled = !isEnglishGB;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LanguageItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
className={styles.languageItem}
|
option={option}
|
||||||
style={{
|
index={index}
|
||||||
opacity: animationTriggered ? 1 : 0,
|
animationTriggered={animationTriggered}
|
||||||
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
isSelected={option.value === i18n.language}
|
||||||
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
fullWidth
|
|
||||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||||
data-selected={option.value === i18n.language}
|
rippleEffect={rippleEffect}
|
||||||
styles={{
|
pendingLanguage={pendingLanguage}
|
||||||
root: {
|
compact={compact}
|
||||||
borderRadius: '4px',
|
disabled={isDisabled}
|
||||||
minHeight: '32px',
|
|
||||||
padding: '4px 8px',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
|
|
||||||
: 'transparent',
|
|
||||||
color: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
|
|
||||||
: 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))',
|
|
||||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: option.value === i18n.language
|
|
||||||
? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))'
|
|
||||||
: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))',
|
|
||||||
transform: 'translateY(-1px)',
|
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: option.value === i18n.language ? 600 : 400,
|
|
||||||
textAlign: 'left',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 2,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
{!compact && rippleEffect && pendingLanguage === option.value && (
|
|
||||||
<div
|
|
||||||
key={rippleEffect.key}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: rippleEffect.x,
|
|
||||||
top: rippleEffect.y,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: 'var(--mantine-color-blue-4)',
|
|
||||||
opacity: 0.6,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
</Button>
|
})}
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
@ -221,3 +296,4 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default LanguageSelector;
|
export default LanguageSelector;
|
||||||
|
export type { LanguageSelectorProps, LanguageOption, RippleEffect };
|
||||||
|
Loading…
x
Reference in New Issue
Block a user